Repository: FormidableLabs/urql
Pull request #1218 (2020-12-10 14:58:43)

(docs) - split up basics by bindings
After [feedback on Twitter](https://twitter.com/its_txbrown/status/1336720698409041920?s=21) we decided to split the basics docs up by framework.

```
Basics
  | React/Preact
  | Vue
  | Svelte
  | Node
  | Document-caching
  | Errors
```

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 20157a04ac9cfb92ef3a08d9fe60e7778c19bd77

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, 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/split-up-docs?filename=.changeset/red-bats-warn.md&value=---%0A%0A---%0A%0A(docs)%20-%20split%20up%20basics%20by%20bindings%0A)



Patch:
diff --git a/.changeset/smart-emus-jam.md b/.changeset/smart-emus-jam.md
new file mode 100644
index 00000000..791401db
--- /dev/null
+++ b/.changeset/smart-emus-jam.md
@@ -0,0 +1,5 @@
+---
+'@urql/exchange-graphcache': major
+---
+
+Add improved error awareness to Graphcache. When Graphcache now receives a `GraphQLError` (via a `CombinedError`) it checks whether the `GraphQLError`'s `path` matches up with `null` values in the `data`. Any `null` values that the write operation now sees in the data will be replaced with a "cache miss" value (i.e. `undefined`) when it has an associated error. This means that errored fields from your GraphQL API will be marked as uncached and won't be cached. Instead the client will now attempt a refetch of the data so that errors aren't preventing future refetches or with schema awareness it will attempt a refetch automatically. Additionally, the `updates` functions will now be able to check whether the current field has any errors associated with it with `info.error`.
diff --git a/docs/README.md b/docs/README.md
index bcff8b23..2449d944 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -11,54 +11,41 @@ GraphQL, as well as extensible, to grow to support dynamic single-app applicatio
 customized GraphQL infrastructure. In short, `urql` prioritizes usability and adaptability.
 
 As you're adopting GraphQL, `urql` becomes your primary data layer and can handle content-heavy
-pages through ["Document Caching"](./basics/document-caching.md) as well as dynamic and data-heavy
+pages through ["Document Caching"](./concepts/document-caching.md) as well as dynamic and data-heavy
 apps through ["Normalized Caching"](./graphcache/normalized-caching.md).
 
-## Constituent Parts
+`urql` can be understood as a collection of connected parts and packages.
+When we only need to install a single package for our framework of choice. We're then able to
+declaratively send GraphQL requests to our API. All framework packages — like `urql` (for React),
+`@urql/preact`, `@urql/svelte`, and `@urql/vue` — wrap the [core package,
+`@urql/core`](./basics/core.md), which we can imagine as the brain
+of `urql` with most of its logic. As we progress with implementing `urql` into our application,
+we're later able to extend it by adding ["addon packages", which we call
+_Exchanges_](./concepts/exchanges.md)
 
-`urql` can be understood as a collection of connected parts and packages. When [getting started](./basics/getting-started.md) we only need to install a single package for our
-framework of choice. We're then able to declaratively send GraphQL requests to our API.
+If at this point you're still unsure of whether to use `urql`, [have a look at the **Comparison**
+page](./comparison.md) and check whether `urql` supports all features you're looking for.
 
-All framework packages — like `urql` (for React), `@urql/preact`, `@urql/svelte`, and `@urql/vue` —
-wrap the [core package, `@urql/core`](./concepts/core-package.md), which we can imagine as the brain
-of `urql` with most of its logic.
+## Where to start
 
-As we progress with implementing `urql` into our application, we're later able to extend it by
-adding ["addon packages", which we call _Exchanges_](./concepts/exchanges.md)
+We have **Getting Started** guides for:
 
-## Quick Start
+- [**React/Preact**](./basics/react-preact.md) covers how to work with the bindings for React/Preact.
+- [**Vue**](./basics/vue.md) covers how to work with the bindings for Vue 3.
+- [**Svelte**](./basics/svelte.md) covers how to work with the bindings for Svelte.
+- [**Core Package**](./basics/core.md) covers the shared "core APIs" and how we can use them directly
+  in Node.js or imperatively.
 
-We have **Getting Started** guides for
-[React &
-Preact](https://formidable.com/open-source/urql/docs/basics/getting-started/#react--preact),
-[Svelte](https://formidable.com/open-source/urql/docs/basics/getting-started/#svelte), and
-[Vue](https://formidable.com/open-source/urql/docs/basics/getting-started/#vue) which walk through
-how to install the bindings for your framework of choice and set up the
-[`Client`](./api/core.md#client).
+Each of these sections will walk you through the specific instructions for the framework bindings,
+including how to install and set them up, how to write queries, and how to send mutations.
 
-Generally for React this would look like this, where the `urql` package may be replaced with your
-framework's bindings:
-
-```sh
-npm i --save urql graphql
-# or
-yarn add urql graphql
-```
-
-The **Basics** section also features pages to [write
-Queries](https://formidable.com/open-source/urql/docs/basics/queries/),
-[Mutations](https://formidable.com/open-source/urql/docs/basics/mutations/), after which we could
-continue learning about
-[Subscriptions](https://formidable.com/open-source/urql/docs/advanced/subscriptions/). These are
-among the only pages that are framework-specific.
-
-## The Documentation
+## Following the Documentation
 
 This documentation is split into groups or sections that cover different levels of usage or areas of
 interest.
 
-- **Basics** is the section where we find the ["Getting Started"
-  guide](./basics/getting-started.md) and usage patterns for our framework of choice.
+- **Basics** is the section where we'll want to start learning about `urql` as it contains "Getting
+  Started" guides for our framework of choice.
 - **Main Concepts** then explains more about how `urql` functions, what it's made up of, and covers
   the main aspects of the `Client` and GraphQL clients in general, on the ["Philosophy"
   page](./concepts/philosophy.md).
@@ -69,7 +56,8 @@ interest.
   use-cases, smarter caching, and more dynamic apps to function.
 - **Showcase** aims to list users of `urql`, third-party packages, and other helpful resources,
   like tutorials and guides.
-- **API** contains a detailed list of all helpers, utilities, components, and other parts of each of
-  `urql`'s packages, which may contain all details of each part and package.
+- **API** contains a detailed documentation on each package's APIs. The documentation links to each
+  of these as appropriate, but if we're unsure of how to use a utility or package, we can go here
+  directly to look up how to use a specific API.
 
 We hope you grow to love `urql`!
diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md
index 6b1edfd4..5e6da947 100644
--- a/docs/advanced/server-side-rendering.md
+++ b/docs/advanced/server-side-rendering.md
@@ -252,14 +252,8 @@ By default `withUrqlClient` will add `getInitialProps` to the component you're w
 `getStaticProps` and `getServerSideProps`. However we can enable this, let's look at an example:
 
 ```js
-import { withUrqlClient, initUrqlClient } from "next-urql";
-import {
-  ssrExchange,
-  dedupExchange,
-  cacheExchange,
-  fetchExchange,
-  useQuery
-} from "urql";
+import { withUrqlClient, initUrqlClient } from 'next-urql';
+import { ssrExchange, dedupExchange, cacheExchange, fetchExchange, useQuery } from 'urql';
 
 const TODOS_QUERY = `
   query { todos { id text } }
@@ -269,7 +263,7 @@ function Todos() {
   const [res] = useQuery({ query: TODOS_QUERY });
   return (
     <div>
-      {res.data.todos.map((todo) => (
+      {res.data.todos.map(todo => (
         <div key={todo.id}>
           {todo.id} - {todo.text}
         </div>
@@ -281,8 +275,8 @@ function Todos() {
 export async function getStaticProps(ctx) {
   const ssrCache = ssrExchange({ isClient: false });
   const client = initUrqlClient({
-    url: "your-url",
-    exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange]
+    url: 'your-url',
+    exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
   });
 
   // This query is used to populate the cache for the query
@@ -292,15 +286,15 @@ export async function getStaticProps(ctx) {
   return {
     props: {
       // urqlState is a keyword here so withUrqlClient can pick it up.
-      urqlState: ssrCache.extractData()
+      urqlState: ssrCache.extractData(),
     },
-    revalidate: 600
+    revalidate: 600,
   };
 }
 
 export default withUrqlClient(
-  (ssr) => ({
-    url: "your-url"
+  ssr => ({
+    url: 'your-url',
   }),
   { ssr: false } // Important so we don't wrap our component in getInitialProps
 )(Todos);
@@ -326,7 +320,7 @@ and on the client, where a replacement loading template is rendered on a parent
 loading.
 
 We've previously seen how we can change our usage of `useQuery`'s `PromiseLike` result to [make use
-of Vue Suspense on the "Queries" page.](../basics/queries.md#vue-suspense)
+of Vue Suspense on the "Queries" page.](../basics/vue.md#vue-suspense)
 
 Any component's `setup()` function can be updated to instead be an `async setup()` function, in
 other words, to return a `Promise` instead of directly returning its data. This means that we can
@@ -358,7 +352,7 @@ const handleRequest = async (req, res) => {
   });
 
   const markup = await renderToString(app);
-  
+
   const data = JSON.stringify(ssr.extractData());
 
   res.status(200).send(`
diff --git a/docs/advanced/subscriptions.md b/docs/advanced/subscriptions.md
index eda5453e..b1f88010 100644
--- a/docs/advanced/subscriptions.md
+++ b/docs/advanced/subscriptions.md
@@ -5,7 +5,7 @@ order: 0
 
 # Subscriptions
 
-One feature of `urql` that was not mentioned in the ["Basics" section](../basics/README.md) is `urql`'s
+One feature of `urql` that was not mentioned in the ["Basics" sections](../basics/README.md) is `urql`'s
 APIs and ability to handle GraphQL subscriptions.
 
 ## The Subscription Exchange
@@ -110,7 +110,7 @@ create a Subscription Observable, which we return to the `subscriptionExchange`
 ## React & Preact
 
 The `useSubscription` hooks comes with a similar API to `useQuery`, which [we've learned about in
-the "Queries" page in the "Basics" section.](../basics/queries.md#react--preact)
+the "Queries" page in the "Basics" section.](../basics/react-preact.md#queries)
 
 Its usage is extremely similar in that it accepts options, which may contain `query` and
 `variables`. However, it also accepts a second argument, which is a reducer function, similar to
@@ -173,7 +173,7 @@ messages.
 ## Svelte
 
 The `subscription` function in `@urql/svelte` comes with a similar API to `query`, which [we've
-learned about in the "Queries" page in the "Basics" section.](../basics/queries.md#svelte)
+learned about in the "Queries" page in the "Basics" section.](../basics/svelte.md#queries)
 
 Its usage is extremely similar in that it accepts an `operationStore`, which will typically contain
 our GraphQL subscription query. However, `subscription` also accepts a second argument, which is
@@ -231,7 +231,7 @@ the list of previous messages.
 ## Vue
 
 The `useSubscription` API is very similar to `useQuery`, which [we've learned about in
-the "Queries" page in the "Basics" section.](../basics/queries.md#vue)
+the "Queries" page in the "Basics" section.](../basics/vue.md#queries)
 
 Its usage is extremely similar in that it accepts options, which may contain `query` and
 `variables`. However, it also accepts a second argument, which is a reducer function, similar to
diff --git a/docs/api/core.md b/docs/api/core.md
index 09d7098b..0fb5431b 100644
--- a/docs/api/core.md
+++ b/docs/api/core.md
@@ -1,6 +1,7 @@
----
+core.md---
 title: '@urql/core'
 order: 0
+
 ---
 
 # @urql/core
@@ -133,7 +134,7 @@ goes wrong during a GraphQL request.
 | `graphQLErrors` | `?Array<string \| GraphQLError>` | GraphQL Errors (if any) that were returned by the GraphQL API                      |
 | `response`      | `?any`                           | The raw response object (if any) from the `fetch` call                             |
 
-[Read more about errors in `urql` on the "Error" page.](../basics/errors.md)
+[Read more about errors in `urql` on the "Error" page.](../concepts/errors.md)
 
 ## Types
 
@@ -193,7 +194,7 @@ handled.
 - `'network-only'`
 - `'cache-and-network'`
 
-[Read more about request policies on the "Queries" page.](../basics/queries.md#request-policies)
+[Read more about request policies on the "Document Caching" page.](../concepts/document-caching.md#request-policies)
 
 ### OperationContext
 
@@ -267,7 +268,7 @@ Patterns".](../concepts/stream-patterns.md)
 
 ### cacheExchange
 
-The `cacheExchange` as [described on the "Document Caching" page.](../basics/document-caching.md). It's of type `Exchange`.
+The `cacheExchange` as [described on the "Document Caching" page.](../concepts/document-caching.md). It's of type `Exchange`.
 
 ### subscriptionExchange
 
diff --git a/docs/api/graphcache.md b/docs/api/graphcache.md
index d22826ec..ce12e46b 100644
--- a/docs/api/graphcache.md
+++ b/docs/api/graphcache.md
@@ -119,6 +119,13 @@ An `UpdateResolver` receives four arguments when it's called: `result`, `args`,
 | `cache`  | `Cache`  | The cache using which data can be read or written. [See `Cache`.](#cache)                                   |
 | `info`   | `Info`   | Additional metadata and information about the current operation and the current field. [See `Info`.](#info) |
 
+It's possible to derive more information about the current update using the `info` argument. For
+instance this metadata contains the current `fieldName` of the updater which may be used to make an
+updater function more reusable, along with `parentKey` and other key fields. It also contains
+`variables` and `fragments` which remain the same for the entire write operation, and additionally
+it may have the `error` field set to describe whether the current field is `null` because the API
+encountered a `GraphQLError`.
+
 [Read more about how to set up `updates` on the "Custom Updates"
 page.](../graphcache/custom-updates.md)
 
@@ -463,17 +470,18 @@ This is a metadata object that is passed to every resolver and updater function.
 information about the current GraphQL document and query, and also some information on the current
 field that a given resolver or updater is called on.
 
-| Argument         | Type                                         | Description                                                                                                                                                  |
-| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `parent`         | `Data`                                       | The field's parent entity's data, as it was written or read up until now, which means it may be incomplete. [Use `cache.resolve`](#resolve) to read from it. |
-| `parentTypeName` | `string`                                     | The field's parent entity's typename                                                                                                                         |
-| `parentKey`      | `string`                                     | The field's parent entity's cache key (if any)                                                                                                               |
-| `parentFieldKey` | `string`                                     | The current key's cache key, which is the parent entity's key combined with the current field's key (This is mostly obsolete)                                |
-| `fieldName`      | `string`                                     | The current field's name                                                                                                                                     |
-| `fragments`      | `{ [name: string]: FragmentDefinitionNode }` | A dictionary of fragments from the current GraphQL document                                                                                                  |
-| `variables`      | `object`                                     | The current GraphQL operation's variables (may be an empty object)                                                                                           |
-| `partial`        | `?boolean`                                   | This may be set to `true` at any point in time (by your custom resolver or by _Graphcache_) to indicate that some data is uncached and missing               |
-| `optimistic`     | `?boolean`                                   | This is only `true` when an optimistic mutation update is running                                                                                            |
+| Argument         | Type                                         | Description                                                                                                                                                                                              |
+| ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `parent`         | `Data`                                       | The field's parent entity's data, as it was written or read up until now, which means it may be incomplete. [Use `cache.resolve`](#resolve) to read from it.                                             |
+| `parentTypeName` | `string`                                     | The field's parent entity's typename                                                                                                                                                                     |
+| `parentKey`      | `string`                                     | The field's parent entity's cache key (if any)                                                                                                                                                           |
+| `parentFieldKey` | `string`                                     | The current key's cache key, which is the parent entity's key combined with the current field's key (This is mostly obsolete)                                                                            |
+| `fieldName`      | `string`                                     | The current field's name                                                                                                                                                                                 |
+| `fragments`      | `{ [name: string]: FragmentDefinitionNode }` | A dictionary of fragments from the current GraphQL document                                                                                                                                              |
+| `variables`      | `object`                                     | The current GraphQL operation's variables (may be an empty object)                                                                                                                                       |
+| `error`          | `GraphQLError \| undefined`                  | The current GraphQLError for a given field. This will always be `undefined` for resolvers and optimistic updaters, but may be present for updaters when the API has returned an error for a given field. |
+| `partial`        | `?boolean`                                   | This may be set to `true` at any point in time (by your custom resolver or by _Graphcache_) to indicate that some data is uncached and missing                                                           |
+| `optimistic`     | `?boolean`                                   | This is only `true` when an optimistic mutation update is running                                                                                                                                        |
 
 > **Note:** Using `info` is regarded as a last resort. Please only use information from it if
 > there's no other solution to get to the metadata you need. We don't regard the `Info` API as
diff --git a/docs/api/request-policy-exchange.md b/docs/api/request-policy-exchange.md
index 5f666375..f2046c51 100644
--- a/docs/api/request-policy-exchange.md
+++ b/docs/api/request-policy-exchange.md
@@ -9,7 +9,7 @@ The `@urql/exchange-request-policy` package contains an addon `requestPolicyExch
 that may be used to upgrade [Operations' Request Policies](./core.md#requestpolicy) on a
 time-to-live basis.
 
-[Read more about request policies on the "Queries" page.](../basics/queries.md#request-policies)
+[Read more about request policies on the "Document Caching" page.](../concepts/document-caching.md#request-policies)
 
 This exchange will conditionally upgrade `cache-first` and `cache-only` operations to use
 `cache-and-network`, so that the client gets an opportunity to update its cached data, when the
diff --git a/docs/api/svelte.md b/docs/api/svelte.md
index a1072ef3..6d64b246 100644
--- a/docs/api/svelte.md
+++ b/docs/api/svelte.md
@@ -56,7 +56,7 @@ variable to it, and instead must pass the raw `operationStore`.
 
 This function will return the `operationStore` itself that has been passed.
 
-[Read more about how to use the `query` API on the "Queries" page.](../basics/queries.md#svelte)
+[Read more about how to use the `query` API on the "Queries" page.](../basics/svelte.md#queries)
 
 ## subscription
 
@@ -94,7 +94,7 @@ The `executeMutation` callback will return a promise which resolves to the `oper
 mutation has been completed.
 
 [Read more about how to use the `mutation` API on the "Mutations"
-page.](../basics/mutations.md#svelte)
+page.](../basics/svelte.md#mutations)
 
 ## Context API
 
diff --git a/docs/api/urql.md b/docs/api/urql.md
index 50d0c5ab..62e51b45 100644
--- a/docs/api/urql.md
+++ b/docs/api/urql.md
@@ -27,7 +27,7 @@ This hook returns a tuple of the shape `[result, executeQuery]`.
   it's called. When `pause` is set to `true` this executes the query, overriding the otherwise
   paused hook.
 
-[Read more about how to use the `useQuery` API on the "Queries" page.](../basics/queries.md)
+[Read more about how to use the `useQuery` API on the "Queries" page.](../basics/react-preact.md#queries)
 
 ## useMutation
 
@@ -40,18 +40,20 @@ Accepts a single `query` argument of type `string | DocumentNode` and returns a
   [`Partial<OperationContext>`](./core.md#operationcontext) and may be used to start executing a
   mutation. It returns a `Promise` resolving to an [`OperationResult`](./core.md#operationresult).
 
-[Read more about how to use the `useMutation` API on the "Mutations" page.](../basics/mutations.md)
+[Read more about how to use the `useMutation` API on the "Mutations"
+page.](../basics/react-preact.md#mutations)
 
 ## useSubscription
 
 Accepts a single required options object as an input with the following properties:
 
-| Prop        | Type                     | Description                                                                                 |
-| ----------- | ------------------------ | ------------------------------------------------------------------------------------------- |
-| `query`     | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode.          |
-| `variables` | `?object`                | The variables to be used with the GraphQL request.                                          |
-| `pause`     | `?boolean`               | A boolean flag instructing [execution to be paused](../basics/queries.md#pausing-usequery). |
-| `context`   | `?object`                | Holds the contextual information for the query.                                             |
+| Prop                                                 | Type                     | Description                                                                        |
+| ---------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
+| `query`                                              | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. |
+| `variables`                                          | `?object`                | The variables to be used with the GraphQL request.                                 |
+| `pause`                                              | `?boolean`               | A boolean flag instructing [execution to be                                        |
+| paused](../basics/react-preact.md#pausing-usequery). |
+| `context`                                            | `?object`                | Holds the contextual information for the query.                                    |
 
 The hook optionally accepts a second argument, which may be a handler function with a type signature
 of:
diff --git a/docs/api/vue.md b/docs/api/vue.md
index cbf6ae3b..c5b7c1b5 100644
--- a/docs/api/vue.md
+++ b/docs/api/vue.md
@@ -14,7 +14,7 @@ Accepts a single required options object as an input with the following properti
 | `query`         | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode.                       |
 | `variables`     | `?object`                | The variables to be used with the GraphQL request.                                                       |
 | `requestPolicy` | `?RequestPolicy`         | An optional [request policy](./core.md#requestpolicy) that should be used specifying the cache strategy. |
-| `pause`         | `?boolean`               | A boolean flag instructing [execution to be paused](../basics/queries.md#pausing-usequery).              |
+| `pause`         | `?boolean`               | A boolean flag instructing [execution to be paused](../basics/vue.md#pausing-usequery).                  |
 | `pollInterval`  | `?number`                | Every `pollInterval` milliseconds the query will be reexecuted.                                          |
 | `context`       | `?object`                | Holds the contextual information for the query.                                                          |
 
@@ -43,7 +43,7 @@ take advantage of [Vue 3's experimental Suspense feature.](https://vuedose.tips/
 When the promise is used, e.g. you `await useQuery(...)` then the `PromiseLike` will only resolve
 once a result from the API is available.
 
-[Read more about how to use the `useQuery` API on the "Queries" page.](../basics/queries.md#vue)
+[Read more about how to use the `useQuery` API on the "Queries" page.](../basics/vue.md#queries)
 
 ## useMutation
 
@@ -59,18 +59,18 @@ The object also carries a special `executeMutation` method, which accepts variab
 mutation. It returns a `Promise` resolving to an [`OperationResult`](./core.md#operationresult)
 
 [Read more about how to use the `useMutation` API on the "Mutations"
-page.](../basics/mutations.md#vue)
+page.](../basics/vue.md#mutations)
 
 ## useSubscription
 
 Accepts a single required options object as an input with the following properties:
 
-| Prop        | Type                     | Description                                                                                 |
-| ----------- | ------------------------ | ------------------------------------------------------------------------------------------- |
-| `query`     | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode.          |
-| `variables` | `?object`                | The variables to be used with the GraphQL request.                                          |
-| `pause`     | `?boolean`               | A boolean flag instructing [execution to be paused](../basics/queries.md#pausing-usequery). |
-| `context`   | `?object`                | Holds the contextual information for the subscription.                                      |
+| Prop        | Type                     | Description                                                                             |
+| ----------- | ------------------------ | --------------------------------------------------------------------------------------- |
+| `query`     | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode.      |
+| `variables` | `?object`                | The variables to be used with the GraphQL request.                                      |
+| `pause`     | `?boolean`               | A boolean flag instructing [execution to be paused](../basics/vue.md#pausing-usequery). |
+| `context`   | `?object`                | Holds the contextual information for the subscription.                                  |
 
 Each of these inputs may also be [reactive](https://v3.vuejs.org/api/refs-api.html) (e.g. a `ref`)
 and are allowed to change over time which will issue a new query.
diff --git a/docs/basics/README.md b/docs/basics/README.md
index 0a7c55b2..a8152a21 100644
--- a/docs/basics/README.md
+++ b/docs/basics/README.md
@@ -6,15 +6,18 @@ order: 2
 # Basics
 
 In this chapter we'll explain the basics of `urql` and how to get started with using it without any
-prior knowledge. It's split into multiple pages that are best read in order.
+prior knowledge.
 
-- [**Getting started**](./getting-started.md) covers how to install the package for your framework of choice and set it up.
-- [**Queries**](./queries.md) explains how to write your first GraphQL queries with `urql`.
-- [**Mutations**](./mutations.md) follows up with how to define GraphQL mutations.
-- [**Document Caching**](./document-caching.md) explains the default cache mechanism of `urql`, as opposed to the opt-in
-  [Normalized Cache](../graphcache/normalized-caching.md).
-- [**Errors**](./errors.md) contains information on error handling in `urql`.
+- [**React/Preact**](./react-preact.md) covers how to work with the bindings for React/Preact.
+- [**Vue**](./vue.md) covers how to work with the bindings for Vue 3.
+- [**Svelte**](./svelte.md) covers how to work with the bindings for Svelte.
+- [**Core Package**](./core-package.md) defines why a shared package exists that contains the main
+  logic of `urql`, and how we can use it directly in Node.js.
 
 After reading "Basics" you may want to [read the "Concepts" chapter of the
 documentation](../concepts/README.md) as it explains the motivation and architecture that drives
-`urql`.
+`urql`. Two notable sections there include:
+
+- [**Document Caching**](../concepts/document-caching.md) explains the default cache mechanism of `urql`, as opposed to the opt-in
+  [Normalized Cache](../graphcache/normalized-caching.md).
+- [**Errors**](../concepts/errors.md) contains information on error handling in `urql`.
diff --git a/docs/basics/core.md b/docs/basics/core.md
new file mode 100644
index 00000000..cda95759
--- /dev/null
+++ b/docs/basics/core.md
@@ -0,0 +1,166 @@
+---
+title: Core
+order: 3
+---
+
+# Core
+
+The `@urql/core` package contains `urql`'s `Client`, some common utilities, and some default
+_Exchanges_. These are the shared, default parts of `urql` that we will be using no matter which
+framework we're interacting with.
+
+All framework bindings — meaning `urql`, `@urql/preact`, `@urql/svelte`, and `@urql/vue` — reexport
+all exports of our `@urql/core` core library. This means that if we want to use `urql`'s `Client`
+imperatively or with Node.js we'd use `@urql/core`'s utilities or the `Client` directly.
+
+## Getting started
+
+Installing `@urql/core` is quick and no other packages are immediately necessary.
+
+```sh
+yarn add @urql/core graphql
+# or
+npm install --save @urql/core graphql
+```
+
+### One-off Queries and Mutations
+
+When you're using `urql` to send one-off queries or mutations — rather than in full framework code,
+where updates are important — it's common to convert the streams that we get to promises. The
+`client.query` and `client.mutation` methods have a shortcut to do just that.
+
+```js
+const QUERY = `
+  query Test($id: ID!) {
+    getUser(id: $id) {
+      id
+      name
+    }
+  }
+`;
+
+client
+  .query(QUERY, { id: 'test' })
+  .toPromise()
+  .then(result => {
+    console.log(result); // { data: ... }
+  });
+```
+
+This may be useful when we don't plan on cancelling queries or we don't care about future updates to
+this data and are just looking to query a result once.
+
+Similarly there's a way to read data from the cache synchronously, provided that the cache has
+received a result for a given query before. The `Client` has a `readQuery` method which is a
+shortcut for just that.
+
+```js
+const QUERY = `
+  query Test($id: ID!) {
+    getUser(id: $id) {
+      id
+      name
+    }
+  }
+`;
+
+const result = client.readQuery(QUERY, { id: 'test' });
+
+result; // null or { data: ... }
+```
+
+Since the streams in `urql` operate synchronously, internally this method subscribes to
+`client.executeQuery` and unsubscribes immediately. If a result is available in the cache it will be
+resolved synchronously prior to the unsubscribe. If not, the query is cancelled and no request will be sent to the GraphQL API.
+
+### gql
+
+A notable utility function is the `gql` tagged template literal function, which is a drop-in
+replacement for `graphql-tag`, if you're coming from other GraphQL clients.
+
+Wherever `urql` accepts a query document, you may either pass a string or a `DocumentNode`. `gql` is
+a utility that allows a `DocumentNode` to be created directly, and others to be interpolated into
+it, which is useful for fragments for instance. This function will often also mark GraphQL documents
+for syntax highlighting in most code editors.
+
+In most examples we may have passed a string to define a query document, like so:
+
+```js
+const TodosQuery = `
+  query {
+    todos {
+      id
+      title
+    }
+  }
+`;
+```
+
+We may also use the `gql` tag function to create a `DocumentNode` directly:
+
+```js
+import { gql } from '@urql/core';
+
+const TodosQuery = gql`
+  query {
+    todos {
+      id
+      title
+    }
+  }
+`;
+```
+
+Since all framework bindings also re-export `@urql/core`, we may also import `gql` from `'urql'`,
+`'@urql/svelte'` and other bindings directly.
+
+We can also start interpolating other documents into the tag function. This is useful to compose
+fragment documents into a larger query, since it's common to define fragments across components of
+an app to spread out data dependencies. If we accidentally use a duplicate fragment name in a
+document, `gql` will log a warning, since GraphQL APIs won't accept duplicate names.
+
+```js
+import { gql } from '@urql/core';
+
+const TodoFragment = gql`
+  fragment SmallTodo on Todo {
+    id
+    title
+  }
+`;
+
+const TodosQuery = gql`
+  query {
+    todos {
+      ...TodoFragment
+    }
+  }
+
+  ${TodoFragment}
+`;
+```
+
+## Common Utilities in Core
+
+The `@urql/core` package contains other utilities that are shared between multiple addon packages.
+This is a short but non-exhaustive list. It contains,
+
+- [`CombinedError`](../api/core.md#combinederror) - our abstraction to combine one or more `GraphQLError`(s) and a `NetworkError`
+- `makeResult` and `makeErrorResult` - utilities to create _Operation Results_
+- [`createRequest`](../api/core.md#createrequest) - a utility function to create a request from a query and some variables (which
+  generates a stable _Operation Key_)
+
+There are other utilities not mentioned here. Read more about the `@urql/core` API in the [API docs](../api/core.md).
+
+## Reading on
+
+This concludes the introduction for using `@urql/core` without any framework bindings. This showed
+just a couple of ways to use `gql` or the `Client`, however you may also want to learn more about
+[how to use `urql`'s streams](../concepts/stream-patterns.md). Furthermore, apart from the framework
+binding introductions, there are some other pages that provide more information on how to get fully
+set up with `urql`:
+
+- [How does the default "document cache" work?](../concepts/document-caching.md)
+- [How are errors handled and represented?](../concepts/errors.md)
+- [A quick overview of `urql`'s philosophy and structure.](../concepts/philosophy.md)
+- [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md)
diff --git a/docs/basics/getting-started.md b/docs/basics/getting-started.md
deleted file mode 100644
index 171a468b..00000000
--- a/docs/basics/getting-started.md
+++ /dev/null
@@ -1,306 +0,0 @@
----
-title: Getting started
-order: 0
----
-
-# Getting started
-
-In the ["Introduction"](./README.md) we read how `urql` consists of multiple constituent parts that
-make up a GraphQL client. Hence there are multiple packages — one for each framework we officially
-support — that we'll get started with on this page.
-
-## React & Preact
-
-This "Getting Started" guide covers how to install and set up `urql` and the `Client`, for React and
-Preact. Since the `urql` and `@urql/preact` packages share most of their API and are used in the
-same way, when reading the documentation on React, all examples are essentially the same, except
-that we'd want to use the `@urql/preact` package instead of the `urql` package for Preact.
-
-### Installation
-
-Installing `urql` is as quick as you'd expect and you won't need any other packages to get started
-with at first. We'll install the package with our package manager of choice.
-
-```sh
-yarn add urql graphql
-# or
-npm install --save urql graphql
-```
-
-To use `urql` with Preact, we have to install `@urql/preact` instead of `urql` and import from
-that package instead. Otherwise all examples for Preact will be the same.
-
-Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
-dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
-to install `graphql` alongside `urql`.
-
-Both the `urql` and `graphql` packages follow [semantic versioning](https://semver.org) and all
-`urql` packages will define a range of compatible versions of `graphql`. Watch out for breaking
-changes in the future however, in which case your package manager may warn you about `graphql` being
-out of the defined peer dependency range.
-
-### Setting up the `Client`
-
-The `urql` and `@urql/preact` packages export a method called `createClient` which we can use to
-create the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
-
-```js
-import { createClient } from 'urql';
-
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-});
-```
-
-At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
-
-Another common option is `fetchOptions`. This option allows us to customize the options that will be
-passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
-a function returning an options object.
-
-In the following example we'll add a token to each `fetch` request that our `Client` sends to our
-GraphQL API.
-
-```js
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-  fetchOptions: () => {
-    const token = getToken();
-    return {
-      headers: { authorization: token ? `Bearer ${token}` : '' },
-    };
-  },
-});
-```
-
-### Providing the `Client`
-
-To make use of the `Client` in React & Preact we will have to provide it via the
-[Context API](https://reactjs.org/docs/context.html). This may be done with the help of
-the `Provider` export.
-
-```jsx
-import { createClient, Provider } from 'urql';
-
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-});
-
-const App = () => (
-  <Provider value={client}>
-    <YourRoutes />
-  </Provider>
-);
-```
-
-Now every component and element inside and under the `Provider` are able to use GraphQL queries that
-will be sent to our API.
-
-[On the next page we'll learn about executing "Queries".](./queries.md#react--preact)
-
-## Svelte
-
-This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for
-Svelte. The `@urql/svelte` package, which provides bindings for Svelte, doesn't fundamentally
-function differently from `@urql/preact` or `urql` and uses the same [Core Package and
-`Client`](../concepts/core-package.md).
-
-### Installation
-
-Installing `@urql/svelte` is quick and no other packages are immediately necessary.
-
-```sh
-yarn add @urql/svelte graphql
-# or
-npm install --save @urql/svelte graphql
-```
-
-Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
-dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
-to install `graphql` alongside `@urql/svelte`.
-
-Both the `@urql/svelte` and `graphql` packages follow [semantic versioning](https://semver.org) and
-all `@urql/svelte` packages will define a range of compatible versions of `graphql`. Watch out
-for breaking changes in the future however, in which case your package manager may warn you about
-`graphql` being out of the defined peer dependency range.
-
-### Setting up the `Client`
-
-The `@urql/svelte` package exports a method called `createClient` which we can use to create
-the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
-
-```js
-import { createClient } from '@urql/svelte';
-
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-});
-```
-
-At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
-
-Another common option is `fetchOptions`. This option allows us to customize the options that will be
-passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
-a function returning an options object.
-
-In the following example we'll add a token to each `fetch` request that our `Client` sends to our
-GraphQL API.
-
-```js
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-  fetchOptions: () => {
-    const token = getToken();
-    return {
-      headers: { authorization: token ? `Bearer ${token}` : '' },
-    };
-  },
-});
-```
-
-### Providing the `Client`
-
-To make use of the `Client` in Svelte we will have to provide it via the
-[Context API](https://svelte.dev/tutorial/context-api). From a parent component to its child
-components. This will share one `Client` with the rest of our app, if we for instance provide the
-`Client`
-
-```html
-<script>
-  import { createClient, setClient } from '@urql/svelte';
-
-  const client = createClient({
-    url: 'http://localhost:3000/graphql',
-  });
-
-  setClient(client);
-</script>
-```
-
-The `setClient` method internally calls [Svelte's `setContext`
-function](https://svelte.dev/docs#setContext). The `@urql/svelte` package also exposes a `getClient`
-function that uses [`getContext`](https://svelte.dev/docs#getContext) to retrieve the `Client` in
-child components. This is used throughout `@urql/svelte`'s API.
-
-We can also use a convenience function, `initClient`. This function combines the `createClient` and
-`setClient` calls into one.
-
-```html
-<script>
-  import { initClient } from '@urql/svelte';
-
-  initClient({
-    url: 'http://localhost:3000/graphql',
-  });
-</script>
-```
-
-[On the next page we'll learn about executing "Queries".](./queries.md#svelte)
-
-## Vue
-
-This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for
-Vue. The `@urql/vue` package, which provides bindings for Vue, doesn't fundamentally
-function differently from `@urql/preact`, or `urql` and uses the same [Core Package and
-`Client`](../concepts/core-package.md).
-
-The `@urql/vue` bindings have been written with [Vue
-3](https://github.com/vuejs/vue-next/releases/tag/v3.0.0) in mind and use Vue's newer [Composition
-API](https://v3.vuejs.org/guide/composition-api-introduction.html). This gives the `@urql/vue`
-bindings capabilities to be more easily integrated into your existing `setup()` functions.
-
-### Installation
-
-Installing `@urql/vue` is quick and no other packages are immediately necessary.
-
-```sh
-yarn add @urql/vue graphql
-# or
-npm install --save @urql/vue graphql
-```
-
-Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
-dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
-to install `graphql` alongside `@urql/vue`.
-
-Both the `@urql/vue` and `graphql` packages follow [semantic versioning](https://semver.org) and
-all `@urql/vue` packages will define a range of compatible versions of `graphql`. Watch out
-for breaking changes in the future however, in which case your package manager may warn you about
-`graphql` being out of the defined peer dependency range.
-
-### Setting up the `Client`
-
-The `@urql/vue` package exports a method called `createClient` which we can use to create
-the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
-
-```js
-import { createClient } from '@urql/vue';
-
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-});
-```
-
-At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
-
-Another common option is `fetchOptions`. This option allows us to customize the options that will be
-passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
-a function returning an options object.
-
-In the following example we'll add a token to each `fetch` request that our `Client` sends to our
-GraphQL API.
-
-```js
-const client = createClient({
-  url: 'http://localhost:3000/graphql',
-  fetchOptions: () => {
-    const token = getToken();
-    return {
-      headers: { authorization: token ? `Bearer ${token}` : '' },
-    };
-  },
-});
-```
-
-### Providing the `Client`
-
-To make use of the `Client` in Vue we will have to provide from a parent component to its child
-components. This will share one `Client` with the rest of our app. In `@urql/vue` there are two
-different ways to achieve this.
-
-The first method is to use `@urql/vue`'s `provideClient` function. This must be called in any of
-your parent components and accepts either a `Client` directly or just the options that you'd pass to
-`createClient`.
-
-```html
-<script>
-  import { createClient, provideClient } from '@urql/vue';
-
-  const client = createClient({
-    url: 'http://localhost:3000/graphql',
-  });
-
-  provideClient(client);
-</script>
-```
-
-Alternatively we may use the exported `install` function and treat `@urql/vue` as a plugin by
-importing its default export and using it [as a plugin](https://v3.vuejs.org/guide/plugins.html#using-a-plugin).
-
-```js
-import { createApp } from 'vue';
-import Root from './App.vue';
-import urql from '@urql/vue';
-
-const app = createApp(Root);
-
-app.use(urql, {
-  url: 'http://localhost:3000/graphql',
-});
-
-app.mount('#app');
-```
-
-The plugin also accepts `createClient`'s options or a `Client` as its inputs.
-
-[On the next page we'll learn about executing "Queries".](./queries.md#vue)
diff --git a/docs/basics/mutations.md b/docs/basics/mutations.md
deleted file mode 100644
index b7ce05af..00000000
--- a/docs/basics/mutations.md
+++ /dev/null
@@ -1,349 +0,0 @@
----
-title: Mutations
-order: 2
----
-
-# Mutations
-
-In this chapter we'll learn how to execute mutations and view their results.
-Sending mutations to our GraphQL API is similar to what we've learned about sending queries to our
-API [previously on the "Queries" page.](./queries.md)
-
-## React & Preact
-
-This guide covers how to send mutations in React and Preact, which share almost the same API.
-
-Both libraries offer a `useMutation` hook and a `Mutation` component. The latter accepts the same
-parameters but we won't cover it in this guide. [Look it up in the API docs if you prefer
-render-props components.](../api/urql.md#mutation-component)
-
-### Sending a mutation
-
-Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
-example! We'll set up a mutation that _updates_ a todo item's title.
-
-```jsx
-const UpdateTodo = `
-  mutation ($id: ID!, $title: String!) {
-    updateTodo (id: $id, title: $title) {
-      id
-      title
-    }
-  }
-`;
-
-const Todo = ({ id, title }) => {
-  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
-};
-```
-
-Similar to the `useQuery` output, `useMutation` returns a tuple. The first item in the tuple again
-contains `fetching`, `error`, and `data` — it's identical since this is a common pattern of how
-`urql` presents [operation results](../api/core.md#operationresult).
-
-Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in
-our example, no mutation will be performed. To execute our mutation we instead have to call the
-execute function — `updateTodo` in our example — which is the second item in the tuple.
-
-### Using the mutation result
-
-When calling our `updateTodo` function we have two ways of getting to the result as it comes back
-from our API. We can either use the first value of the returned tuple — our `updateTodoResult` — or
-we can use the promise that `updateTodo` returns.
-
-```jsx
-const Todo = ({ id, title }) => {
-  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
-
-  const submit = newTitle => {
-    const variables = { id, title: newTitle || '' };
-    updateTodo(variables).then(result => {
-      // The result is almost identical to `updateTodoResult` with the exception
-      // of `result.fetching` not being set.
-      // It is an OperationResult.
-    });
-  };
-};
-```
-
-The result is useful when your UI has to display progress on the mutation, and the returned
-promise is particularly useful when you're adding side-effects that run after the mutation has
-completed.
-
-### Handling mutation errors
-
-It's worth noting that the promise we receive when calling the execute function will never
-reject. Instead it will always return a promise that resolves to a result.
-
-If you're checking for errors, you should use `result.error` instead, which will be set
-to a `CombinedError` when any kind of errors occurred while executing your mutation.
-[Read more about errors on our "Errors" page.](./errors.md)
-
-```jsx
-const Todo = ({ id, title }) => {
-  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
-
-  const submit = newTitle => {
-    const variables = { id, title: newTitle || '' };
-    updateTodo(variables).then(result => {
-      if (result.error) {
-        console.error('Oh no!', result.error);
-      }
-    });
-  };
-};
-```
-
-There are some more tricks we can use with `useMutation`.<br />
-[Read more about its API in the API docs for it.](../api/urql.md#usemutation)
-
-[On the next page we'll learn about "Document Caching", `urql`'s default caching
-mechanism.](./document-caching.md)
-
-## Svelte
-
-This guide covers how to send mutations in Svelte using `@urql/svelte`'s `mutation` utility.
-The `mutation` function isn't dissimilar from the `query` function but is triggered manually and
-can accept a [`GraphQLRequest` object](../api/core.md#graphqlrequest) too while also supporting our
-trusty `operationStore`.
-
-### Sending a mutation
-
-Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
-example! We'll set up a mutation that _updates_ a todo item's title.
-
-```html
-<script>
-  import { mutation } from '@urql/svelte';
-
-  export let id;
-
-  const mutateTodo = mutation({
-    query: `
-      mutation ($id: ID!, $title: String!) {
-        updateTodo (id: $id, title: $title) {
-          id
-          title
-        }
-      }
-    `,
-  });
-
-  function updateTodo(newTitle) {
-    mutateTodo({ id, title: newTitle });
-  }
-</script>
-```
-
-This small call to `mutation` accepts a `query` property (besides the `variables` property) and
-returns an execute function. We've wrapped it in an `updateTodo` function to illustrate its usage.
-
-Unlike the `query` function, the `mutation` function doesn't start our mutation automatically.
-Instead, mutations are started programmatically by calling the function they return. This function
-also returns a promise so that we can use the mutation's result.
-
-### Using the mutation result
-
-When calling `mutateTodo` in our previous example, we start the mutation. To use the mutation's
-result we actually have two options instead of one.
-
-The first option is to use the promise that the `mutation`'s execute function returns. This promise
-will resolve to an `operationStore`, which is what we're used to from sending queries. Using this
-store we can then read the mutation's `data` or `error`.
-
-```html
-<script>
-  import { mutation } from '@urql/svelte';
-
-  export let id;
-
-  const mutateTodo = mutation({
-    query: `
-      mutation ($id: ID!, $title: String!) {
-        updateTodo (id: $id, title: $title) {
-          id
-          title
-        }
-      }
-    `,
-  });
-
-  function updateTodo(newTitle) {
-    mutateTodo({ id, title: newTitle }).then(result => {
-      // The result is an operationStore again, which will already carry the mutation's result
-      console.log(result.data, result.error);
-    });
-  }
-</script>
-```
-
-Alternatively, we can pass `mutation` an `operationStore` directly. This allows us to use a
-mutation's result in our component's UI more easily, without storing it ourselves.
-
-```html
-<script>
-  import { operationStore, mutation } from '@urql/svelte';
-
-  export let id;
-
-  const updateTodoStore = operationStore(`
-    mutation ($id: ID!, $title: String!) {
-      updateTodo (id: $id, title: $title) {
-        id
-        title
-      }
-    }
-  `);
-
-  const updateTodoMutation = mutation(updateTodoStore);
-
-  function updateTodo(newTitle) {
-    updateTodoMutation({ id, title: newTitle });
-  }
-</script>
-
-{#if $updateTodoStore.data} Todo was updated! {/if}
-```
-
-### Handling mutation errors
-
-It's worth noting that the promise we receive when calling the execute function will never
-reject. Instead it will always return a promise that resolves to an `operationStore`, even if the
-mutation has failed.
-
-If you're checking for errors, you should use `operationStore.error` instead, which will be set
-to a `CombinedError` when any kind of errors occurred while executing your mutation.
-[Read more about errors on our "Errors" page.](./errors.md)
-
-```jsx
-mutateTodo({ id, title: newTitle }).then(result => {
-  if (result.error) {
-    console.error('Oh no!', result.error);
-  }
-});
-```
-
-[On the next page we'll learn about "Document Caching", `urql`'s default caching
-mechanism.](./document-caching.md)
-
-## Vue
-
-This guide covers how to send mutations in Vue using `@urql/vue`'s `useMutation` API.
-The `useMutation` function isn't dissimilar from `useQuery` but is triggered manually and accepts
-only a `DocumentNode` or `string` as an input.
-
-### Sending a mutation
-
-Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
-example! We'll set up a mutation that _updates_ a todo item's title.
-
-```js
-import { useMutation } from '@urql/vue';
-
-export default {
-  setup() {
-    const { executeMutation: updateTodo } = useMutation(`
-      mutation ($id: ID!, $title: String!) {
-        updateTodo (id: $id, title: $title) {
-          id
-          title
-        }
-      }
-    `);
-
-    return { updateTodo };
-  },
-};
-```
-
-Similar to the `useQuery` output, `useMutation` returns a result object, which reflects the data of
-an executed mutation. That means it'll contain the familiar `fetching`, `error`, and `data`
-properties — it's identical since this is a common pattern of how `urql`
-presents [operation results](../api/core.md#operationresult).
-
-Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in
-our example, no mutation will be performed. To execute our mutation we instead have to call the
-`executeMutation` method on the result with some variables.
-
-### Using the mutation result
-
-When calling our `updateTodo` function we have two ways of getting to the result as it comes back
-from our API. We can either use the result itself, since all properties related to the last
-[operation result](../api/core.md#operationresult) are marked as [reactive
-](https://v3.vuejs.org/guide/reactivity-fundamentals.html) — or we can use the promise that the
-`executeMutation` method returns when it's called:
-
-```js
-import { useMutation } from '@urql/vue';
-
-export default {
-  setup() {
-    const updateTodoResult = useMutation(`
-      mutation ($id: ID!, $title: String!) {
-        updateTodo (id: $id, title: $title) {
-          id
-          title
-        }
-      }
-    `);
-
-    return {
-      updateTodo(id, title) {
-        const variables = { id, title: title || '' };
-        updateTodoResult(variables).then(result => {
-          // The result is almost identical to `updateTodoResult` with the exception
-          // of `result.fetching` not being set and its properties not being reactive.
-          // It is an OperationResult.
-        });
-      },
-    };
-  },
-};
-```
-
-The reactive result that `useMutation` returns is useful when your UI has to display progress or
-results on the mutation, and the returned promise is particularly useful when you're adding
-side-effects that run after the mutation has completed.
-
-### Handling mutation errors
-
-It's worth noting that the promise we receive when calling the execute function will never
-reject. Instead it will always return a promise that resolves to a result.
-
-If you're checking for errors, you should use `result.error` instead, which will be set
-to a `CombinedError` when any kind of errors occurred while executing your mutation.
-[Read more about errors on our "Errors" page.](./errors.md)
-
-```js
-import { useMutation } from '@urql/vue';
-
-export default {
-  setup() {
-    const updateTodoResult = useMutation(`
-      mutation ($id: ID!, $title: String!) {
-        updateTodo (id: $id, title: $title) {
-          id
-          title
-        }
-      }
-    `);
-
-    return {
-      updateTodo(id, title) {
-        const variables = { id, title: title || '' };
-        updateTodoResult(variables).then(result => {
-          if (result.error) {
-            console.error('Oh no!', result.error);
-          }
-        });
-      },
-    };
-  },
-};
-```
-
-There are some more tricks we can use with `useMutation`.<br />
-[Read more about its API in the API docs for it.](../api/vue.md#usemutation)
-
-[On the next page we'll learn about "Document Caching", `urql`'s default caching
-mechanism.](./document-caching.md)
diff --git a/docs/basics/queries.md b/docs/basics/queries.md
deleted file mode 100644
index 6ce36542..00000000
--- a/docs/basics/queries.md
+++ /dev/null
@@ -1,901 +0,0 @@
----
-title: Queries
-order: 1
----
-
-# Queries
-
-Let's get to querying our data! This page will teach us how we can retrieve our data declaratively
-from our GraphQL API with the help of `urql`.
-
-## React & Preact
-
-This guide covers how to query data with React and Preact, which share almost the same API.
-
-Both libraries offer a `useQuery` hook and a `Query` component. The latter accepts the same
-parameters but we won't cover it in this guide. [Look it up in the API docs if you prefer
-render-props components.](../api/urql.md#query-component)
-
-### Run a first query
-
-For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
-todo items. Let's dive right into it!
-
-```jsx
-import { useQuery } from 'urql';
-
-const TodosQuery = `
-  query {
-    todos {
-      id
-      title
-    }
-  }
-`;
-
-const Todos = () => {
-  const [result, reexecuteQuery] = useQuery({
-    query: TodosQuery,
-  });
-
-  const { data, fetching, error } = result;
-
-  if (fetching) return <p>Loading...</p>;
-  if (error) return <p>Oh no... {error.message}</p>;
-
-  return (
-    <ul>
-      {data.todos.map(todo => (
-        <li key={todo.id}>{todo.title}</li>
-      ))}
-    </ul>
-  );
-};
-```
-
-Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts
-options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The
-tuple we then get in return is an array that contains a result object and a re-execute function.
-
-The result object contains several properties. The `fetching` field indicates whether we're currently
-loading data, `data` contains the actual `data` from the API's result, and `error` is set when either
-the request to the API has failed or when our API result contained some `GraphQLError`s, which
-we'll get into later on the ["Errors" page](./errors.md).
-
-### Variables
-
-Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
-pagination. For this purpose the `useQuery` hook also accepts a `variables` option, which we can use
-to supply variables to our query.
-
-```jsx
-const TodosListQuery = `
-  query ($from: Int!, $limit: Int!) {
-    todos (from: $from, limit: $limit) {
-      id
-      title
-    }
-  }
-`;
-
-const Todos = ({ from, limit }) => {
-  const [result, reexecuteQuery] = useQuery({
-    query: TodosListQuery,
-    variables: { from, limit },
-  });
-
-  // ...
-};
-```
-
-As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
-`POST` request body that is sent to our GraphQL API.
-
-Whenever the `variables` (or the `query`) option on the `useQuery` hook changes `fetching` will
-switch to `true` and a new request will be sent to our API, unless a result has already been cached
-previously.
-
-### Pausing `useQuery`
-
-In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not
-execute the query otherwise. For instance, we may be building a form and want a validation query to
-only take place when a field has been filled out.
-
-Since hooks in React can't just be commented out, the `useQuery` hook accepts a `pause` option that
-temporarily _freezes_ all changes and stops requests.
-
-In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit`
-variables have been defined to be non-nullable `Int!` values.
-
-Let's pause the query we've just
-written to not execute when these variables are empty, to prevent `null` variables from being
-executed. We can do this by means of setting the `pause` option to `true`:
-
-```jsx
-const Todos = ({ from, limit }) => {
-  const [result, reexecuteQuery] = useQuery({
-    query: TodosListQuery,
-    variables: { from, limit },
-    pause: !from || !limit,
-  });
-
-  // ...
-};
-```
-
-Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed.
-This also means that `result.data` won't change, which means we'll still have access to our old data
-even though the variables may have changed.
-
-### Request Policies
-
-As has become clear in the previous sections of this page, the `useQuery` hook accepts more options
-than just `query` and `variables`. Another option we should touch on is `requestPolicy`.
-
-The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
-default this is set to `cache-first`, which means that we prefer to get results from our cache, but
-are falling back to sending an API request.
-
-In total there are four different policies that we can use:
-
-- `cache-first` (the default) prefers cached results and falls back to sending an API request when
-  no prior result is cached.
-- `cache-and-network` returns cached results but also always sends an API request, which is perfect
-  for displaying data quickly while keeping it up-to-date.
-- `network-only` will always send an API request and will ignore cached results.
-- `cache-only` will always return cached results or `null`.
-
-The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
-if it has been cached, but also refreshes data in our cache in the background. This means though
-that `fetching` will be `false` for cached results although an API request may still be ongoing in
-the background.
-
-For this reason there's another field on results, `result.stale`, which indicates that the cached
-result is either outdated or that another request is being sent in the background.
-
-Request policies aren't specific to `urql`'s React API, but are a common feature in its core. [You
-can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
-
-### Reexecuting Queries
-
-The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or
-`variables` change, but in some cases we may find that we need to programmatically trigger a new
-query. This is the purpose of the `reexecuteQuery` function which is the second item in the tuple
-that `useQuery` returns.
-
-Triggering a query programmatically may be useful in a couple of cases. It can for instance be used
-to refresh data that is currently being displayed. In these cases we may also override the
-`requestPolicy` of our query just once and set it to `network-only` to skip the cache.
-
-```jsx
-const Todos = ({ from, limit }) => {
-  const [result, reexecuteQuery] = useQuery({
-    query: TodosListQuery,
-    variables: { from, limit },
-  });
-
-  const refresh = () => {
-    // Refetch the query and skip the cache
-    reexecuteQuery({ requestPolicy: 'network-only' });
-  };
-};
-```
-
-Calling `refresh` in the above example will execute the query again forcefully, and will skip the
-cache, since we're passing `requestPolicy: 'network-only'`.
-
-Furthermore the `reexecuteQuery` function can also be used to programmatically start a query even
-when `pause` is set to `true`, which would usually stop all automatic queries.
-
-### Reading on
-
-There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
-it.](../api/urql.md#usequery)
-
-[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#react--preact)
-
-## Svelte
-
-This guide covers how to query data with Svelte with our `Client` now fully set up and provided via
-the Context API. We'll implement queries using the `operationStore` and the `query` function from
-`@urql/svelte`.
-
-The `operationStore` function creates a [Svelte Writable store](https://svelte.dev/docs#writable).
-You can use it to initialise a data container in `urql`. This store holds on to our query inputs,
-like the GraphQL query and variables, which we can change to launch new queries, and also exposes
-the query's eventual result, which we can then observe.
-
-### Run a first query
-
-For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
-todo items. Let's dive right into it!
-
-```jsx
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todos = operationStore(`
-    query {
-      todos {
-        id
-        title
-      }
-    }
-  `);
-
-  query(todos);
-</script>
-
-{#if $todos.fetching}
-<p>Loading...</p>
-{:else if $todos.error}
-<p>Oh no... {$todos.error.message}</p>
-{:else}
-<ul>
-  {#each $todos.data.todos as todo}
-  <li>{todo.title}</li>
-  {/each}
-</ul>
-{/if}
-```
-
-Here we have implemented our first GraphQL query to fetch todos. We're first creating an
-`operationStore` which holds on to our `query` and are then passing the store to the `query`
-function, which starts the GraphQL query.
-
-The `todos` store can now be used like any other Svelte store using a
-[reactive auto-subscription](https://svelte.dev/tutorial/auto-subscriptions) in Svelte. This means
-that we prefix `$todos` with a dollar symbol, which automatically subscribes us to its changes.
-
-The `query` function accepts our store and starts using the `Client` to execute our query. It may
-only be called once for a store and lives alongside the component's lifecycle. It will automatically
-read changes on the `operationStore` and will update our query and results accordingly.
-
-### Variables
-
-Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
-pagination. For this purpose the `operationStore` also accepts a `variables` argument, which we can
-use to supply variables to our query.
-
-```jsx
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todos = operationStore(`
-    query ($from: Int!, $limit: Int!) {
-      todos(from: $from, limit: $limit) {
-        id
-        title
-      }
-    }`,
-    { from, limit }
-  );
-
-  query(todos);
-</script>
-
-...
-```
-
-As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
-`POST` request body that is sent to our GraphQL API.
-
-The `operationStore` also supports being actively changed. This will hook into Svelte's reactivity
-model as well and cause the `query` utility to start a new operation.
-
-```jsx
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todos = operationStore(`
-    query ($from: Int!, $limit: Int!) {
-      todos(from: $from, limit: $limit) {
-        id
-        title
-      }
-    }`,
-    { from, limit }
-  );
-
-  query(todos);
-
-  function nextPage() {
-    $todos.variables.from += $todos.variables.limit;
-  }
-</script>
-
-<button on:click={nextPage}>Next page<button></button></button>
-```
-
-The `operationStore` provides getters too so it's also possible for us to pass `todos` around and
-update `todos.variables` or `todos.query` directly. Both, updating `todos.variables` and
-`$todos.variables` in a component for instance, will cause `query` to pick up the update and execute
-our changes.
-
-### Pausing Queries
-
-In some cases we may want our queries to not execute until a pre-condition has been met. Since the
-`query` operation exists for the entire component lifecycle however, it can't just be stopped and
-started at will. Instead, the `query`'s third argument, the `context`, may have an added `pause`
-option that can be set to `true` to temporarily _freeze_ all changes and stop requests.
-
-For instance we may start out with a paused store and then unpause it once a callback is invoked:
-
-```html
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todo = operationStore(
-    `
-    query {
-      todo {
-        id
-        title
-      }
-    }`,
-    undefined,
-    { pause: true }
-  );
-
-  query(todo);
-
-  function unpause() {
-    $todo.context.pause = false;
-  }
-</script>
-
-<button on:click={unpause}>Unpause</button>
-```
-
-### Request Policies
-
-The `operationStore` also accepts another argument apart from `query` and `variables`. Optionally
-you may pass a third argument, [the `context` object](../api/core.md#operationcontext). The arguably
-most interesting option the `context` may contain is `requestPolicy`.
-
-The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
-default this is set to `cache-first`, which means that we prefer to get results from our cache, but
-are falling back to sending an API request.
-
-In total there are four different policies that we can use:
-
-- `cache-first` (the default) prefers cached results and falls back to sending an API request when
-  no prior result is cached.
-- `cache-and-network` returns cached results but also always sends an API request, which is perfect
-  for displaying data quickly while keeping it up-to-date.
-- `network-only` will always send an API request and will ignore cached results.
-- `cache-only` will always return cached results or `null`.
-
-The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
-if it has been cached, but also refreshes data in our cache in the background. This means though
-that `fetching` will be `false` for cached results although an API request may still be ongoing in
-the background.
-
-For this reason there's another field on results, `result.stale`, which indicates that the cached
-result is either outdated or that another request is being sent in the background.
-
-```jsx
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todos = operationStore(`
-    query ($from: Int!, $limit: Int!) {
-      todos(from: $from, limit: $limit) {
-        id
-        title
-      }
-    }`,
-    { from, limit },
-    { requestPolicy: 'cache-and-network' }
-  );
-
-  query(todos);
-</script>
-
-...
-```
-
-As we can see, the `requestPolicy` is easily changed here and we can read our `context` option back
-from `todos.context`, just as we can check `todos.query` and `todos.variables`. Updating
-`operationStore.context` can be very useful to also refetch queries, as we'll see in the next
-section.
-
-[You can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
-
-### Reexecuting Queries
-
-The default caching approach in `@urql/svelte` typically takes care of updating queries on the fly
-quite well and does so automatically. Sometimes it may be necessary though to refetch data and to
-execute a query with a different `context`. Triggering a query programmatically may be useful in a
-couple of cases. It can for instance be used to refresh data that is currently being displayed.
-
-We can trigger a new query update by changing out the `context` of our `operationStore`.
-
-```jsx
-<script>
-  import { operationStore, query } from '@urql/svelte';
-
-  const todos = operationStore(`
-    query ($from: Int!, $limit: Int!) {
-      todos(from: $from, limit: $limit) {
-        id
-        title
-      }
-    }`,
-    { from, limit },
-    { requestPolicy: 'cache-first' }
-  );
-
-  query(todos);
-
-  function refresh() {
-    $todos.context = { requestPolicy: 'network-only' };
-  }
-</script>
-```
-
-Calling `refresh` in the above example will execute the query again forcefully, and will skip the
-cache, since we're passing `requestPolicy: 'network-only'`.
-
-### Reading on
-
-There are some more tricks we can use with `operationStore`.
-[Read more about its API in the API docs for it.](../api/svelte.md#operationStore)
-
-[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#svelte)
-
-## Vue
-
-This guide covers how to query data with Vue with our `Client` now fully set up and provided to an
-app. We'll implement queries using the `useQuery` function from `@urql/vue`.
-
-### Run a first query
-
-For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
-todo items. Let's dive right into it!
-
-```jsx
-<template>
-  <div v-if="fetching">
-    Loading...
-  </div>
-  <div v-else-if="error">
-    Oh no... {{error}}
-  </div>
-  <div v-else>
-    <ul v-if="data">
-      <li v-for="todo in data.todos">{{ todo.title }}</li>
-    </ul>
-  </div>
-</template>
-
-<script>
-import { useQuery } from '@urql/vue';
-
-export default {
-  setup() {
-    const result = useQuery({
-      query: `
-        {
-          todos {
-            id
-            title
-          }
-        }
-      `
-    });
-
-    return {
-      fetching: result.fetching,
-      data: result.data,
-      error: result.error,
-    };
-  }
-};
-</script>
-```
-
-Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts
-options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The
-tuple we then get in return is an array that contains a result object and a re-execute function.
-
-The result object contains several properties. The `fetching` field indicates whether we're currently
-loading data, `data` contains the actual `data` from the API's result, and `error` is set when either
-the request to the API has failed or when our API result contained some `GraphQLError`s, which
-we'll get into later on the ["Errors" page](./errors.md).
-
-All of these properties on the result are derived from the [shape of
-`OperationResult`](../api/core.md#operationresult) and are marked as [reactive
-](https://v3.vuejs.org/guide/reactivity-fundamentals.html), which means they may
-update while the query is running, which will automatically update your UI.
-
-### Variables
-
-Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
-pagination. For this purpose `useQuery` also accepts a `variables` input, which we can
-use to supply variables to our query.
-
-```jsx
-<template>
-  ...
-</template>
-
-<script>
-import { useQuery } from '@urql/vue';
-
-export default {
-  props: ['from', 'limit'],
-  setup({ from, limit }) {
-    return useQuery({
-      query: `
-        query ($from: Int!, $limit: Int!) {
-          todos(from: $from, limit: $limit) {
-            id
-            title
-          }
-        }
-      `,
-      variables: { from, limit }
-    });
-  }
-};
-</script>
-```
-
-As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
-`POST` request body that is sent to our GraphQL API.
-
-All inputs that are passed to `useQuery` may also be [reactive
-state](https://v3.vuejs.org/guide/reactivity-fundamentals.html). This means that both the inputs and
-outputs of `useQuery` are reactive and may change over time.
-
-```jsx
-<template>
-  <ul v-if="data">
-    <li v-for="todo in data.todos">{{ todo.title }}</li>
-  </ul>
-  <button @click="from += 10">Next Page</button>
-</template>
-
-<script>
-import { useQuery } from '@urql/vue';
-
-export default {
-  setup() {
-    const from = ref(0);
-
-    const result = useQuery({
-      query: `
-        query ($from: Int!, $limit: Int!) {
-          todos(from: $from, limit: $limit) {
-            id
-            title
-          }
-        }
-      `,
-      variables: { from, limit: 10 }
-    });
-
-    return {
-      from,
-      data: result.data,
-    };
-  }
-};
-</script>
-```
-
-### Pausing `useQuery`
-
-In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not
-execute the query otherwise. For instance, we may be building a form and want a validation query to
-only take place when a field has been filled out.
-
-Since with Vue 3's Composition API we won't just conditionally call `useQuery` we can instead pass a
-reactive `pause` input to `useQuery`.
-
-In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit`
-variables have been defined to be non-nullable `Int!` values.
-
-Let's pause the query we've just written to not execute when these variables are empty, to
-prevent `null` variables from being executed. We can do this by computing `pause` to become `true`
-whenever these variables are falsy:
-
-```js
-import { reactive } from 'vue'
-import { useQuery } from '@urql/vue';
-
-export default {
-  props: ['from', 'limit'],
-  setup({ from, limit }) {
-    return useQuery({
-      query: `
-        query ($from: Int!, $limit: Int!) {
-          todos(from: $from, limit: $limit) {
-            id
-            title
-          }
-        }
-      `,
-      variables: { from, limit },
-      pause: computed(() => !from.value || !limit.value)
-    });
-  }
-};
-</script>
-```
-
-Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed.
-This also means that `result.data` won't change, which means we'll still have access to our old data
-even though the variables may have changed.
-
-It's worth noting that depending on whether `from` and `limit` are reactive or not you may have to
-change how `pause` is computed. But there's also an imperative alternative to this API. Not only
-does the result you get back from `useQuery` have an `isPaused` ref, it also has `pause()` and
-`resume()` methods.
-
-```jsx
-<template>
-  <div v-if="fetching">
-    Loading...
-  </div>
-  <button @click="isPaused ? resume() : pause()">Toggle Query</button>
-</template>
-
-<script>
-import { useQuery } from '@urql/vue';
-
-export default {
-  setup() {
-    return useQuery({
-      query: `
-        {
-          todos {
-            id
-            title
-          }
-        }
-      `
-    });
-  }
-};
-</script>
-```
-
-This means that no matter whether you're in or outside of `setup()` or rather supplying the inputs
-to `useQuery` or using the outputs, you'll have access to ways to pause or unpause the query.
-
-### Request Policies
-
-As has become clear in the previous sections of this page, the `useQuery` hook accepts more options
-than just `query` and `variables`. Another option we should touch on is `requestPolicy`.
-
-The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
-default this is set to `cache-first`, which means that we prefer to get results from our cache, but
-are falling back to sending an API request.
-
-In total there are four different policies that we can use:
-
-- `cache-first` (the default) prefers cached results and falls back to sending an API request when
-  no prior result is cached.
-- `cache-and-network` returns cached results but also always sends an API request, which is perfect
-  for displaying data quickly while keeping it up-to-date.
-- `network-only` will always send an API request and will ignore cached results.
-- `cache-only` will always return cached results or `null`.
-
-The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
-if it has been cached, but also refreshes data in our cache in the background. This means though
-that `fetching` will be `false` for cached results although an API request may still be ongoing in
-the background.
-
-For this reason there's another field on results, `result.stale`, which indicates that the cached
-result is either outdated or that another request is being sent in the background.
-
-Request policies aren't specific to `urql`'s Vue bindings, but are a common feature in its core. [You
-can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
-
-### Reexecuting Queries
-
-The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or
-`variables` change, but in some cases we may find that we need to programmatically trigger a new
-query. This is the purpose of the `executeQuery` method which is a method on the result object
-that `useQuery` returns.
-
-Triggering a query programmatically may be useful in a couple of cases. It can for instance be used
-to refresh data that is currently being displayed. In these cases we may also override the
-`requestPolicy` of our query just once and set it to `network-only` to skip the cache.
-
-```js
-import { useQuery } from '@urql/vue';
-
-export default {
-  setup() {
-    const result = useQuery({
-      query: `
-        {
-          todos {
-            id
-            title
-          }
-        }
-      `
-    });
-
-    return {
-      data: result.data,
-      fetching: result.fetching,
-      error: result.error,
-      refresh() {
-        result.executeQuery({
-          requestPolicy: 'network-only'
-        });
-      }
-    };
-  }
-};
-</script>
-```
-
-Calling `refresh` in the above example will execute the query again forcefully, and will skip the
-cache, since we're passing `requestPolicy: 'network-only'`.
-
-Furthermore the `executeQuery` method can also be used to programmatically start a query even
-when `pause` is `true`, which would usually stop all automatic queries.
-
-### Vue Suspense
-
-In Vue 3 a [new feature was introduced](https://vuedose.tips/go-async-in-vue-3-with-suspense/) that
-natively allows components to suspend while data is loading, which works universally on the server
-and on the client, where a replacement loading template is rendered on a parent while data is
-loading.
-
-Any component's `setup()` function can be updated to instead be an `async setup()` function, in
-other words, to return a `Promise` instead of directly returning its data. This means that we can
-update any `setup()` function to make use of Suspense.
-
-The `useQuery`'s returned result supports this, since it is a `PromiseLike`. We can update one of
-our examples to have a suspending component by changing our usage of `useQuery`:
-
-```jsx
-<template>
-  <ul>
-    <li v-for="todo in data.todos">{{ todo.title }}</li>
-  </ul>
-</template>
-
-<script>
-import { useQuery } from '@urql/vue';
-
-export default {
-  async setup() {
-    const { data, error } = await useQuery({
-      query: `
-        {
-          todos {
-            id
-            title
-          }
-        }
-      `
-    });
-
-    return { data };
-  }
-};
-</script>
-```
-
-As we can see, `await useQuery(...)` here suspends the component and what we render will not have to
-handle the loading states of `useQuery` at all. Instead in Vue Suspense we'll have to wrap a parent
-component in a "Suspense boundary." This boundary is what switches a parent to a loading state while
-parts of its children are fetching data. The suspense promise is in essence "bubbling up" until it
-finds a "Suspense boundary".
-
-```
-<template>
- <Suspense>
-   <template #default>
-     <MyAsyncComponent />
-   </template>
-   <template #fallback>
-     <span>Loading...</span>
-   </template>
- </Suspense>
-</template>
-```
-
-As long as any parent component is wrapping our component which uses `async setup()` in this
-boundary, we'll get Vue Suspense to work correctly and trigger this loading state. When a child
-suspends this component will switch to using its `#fallback` template rather than its `#default`
-template.
-
-### Reading on
-
-There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
-it.](../api/vue.md#usequery)
-
-[On the next page we'll learn about "Mutations" rather than Queries.](./mutations.md#vue)
-
-## Core
-
-All framework bindings — meaning `urql`, `@urql/preact`, `@urql/svelte`, and `@urql/vue` — reexport
-all exports of our `@urql/core` core library. This package contains the
-[`Client`](../api/core.md#client), built-in exchanges, and other utilities that are shared between
-all bindings.
-
-### gql
-
-A notable utility function is the `gql` tagged template literal function, which is a drop-in
-replacement for `graphql-tag`, if you're coming from other GraphQL clients.
-
-Wherever `urql` accepts a query document, you may either pass a string or a `DocumentNode`. `gql` is
-a utility that allows a `DocumentNode` to be created directly, and others to be interpolated into
-it, which is useful for fragments for instance. This function will often also mark GraphQL documents
-for syntax highlighting in most code editors.
-
-In most examples we may have passed a string to define a query document, like so:
-
-```js
-const TodosQuery = `
-  query {
-    todos {
-      id
-      title
-    }
-  }
-`;
-```
-
-We may also use the `gql` tag function to create a `DocumentNode` directly:
-
-```js
-import { gql } from '@urql/core';
-
-const TodosQuery = gql`
-  query {
-    todos {
-      id
-      title
-    }
-  }
-`;
-```
-
-Since all framework bindings also re-export `@urql/core`, we may also import `gql` from `'urql'`,
-`'@urql/svelte'` and other bindings directly.
-
-We can also start interpolating other documents into the tag function. This is useful to compose
-fragment documents into a larger query, since it's common to define fragments across components of
-an app to spread out data dependencies. If we accidentally use a duplicate fragment name in a
-document, `gql` will log a warning, since GraphQL APIs won't accept duplicate names.
-
-```js
-import { gql } from '@urql/core';
-
-const TodoFragment = gql`
-  fragment SmallTodo on Todo {
-    id
-    title
-  }
-`;
-
-const TodosQuery = gql`
-  query {
-    todos {
-      ...TodoFragment
-    }
-  }
-
-  ${TodoFragment}
-`;
-```
-
-### Reading on
-
-There are some more utilities that `@urql/core` exports. [All of them are listed in the API docs for
-it.](../api/core.md)
-
-[Read more about the `@urql/core` library on the "Core Package"
-page.](https://formidable.com/open-source/urql/docs/concepts/core-package/)
-
-
diff --git a/docs/basics/react-preact.md b/docs/basics/react-preact.md
new file mode 100644
index 00000000..5624325e
--- /dev/null
+++ b/docs/basics/react-preact.md
@@ -0,0 +1,355 @@
+---
+title: React/Preact
+order: 0
+---
+
+# React/Preact
+
+This guide covers how to install and setup `urql` and the `Client`, as well as query and mutate data,
+with React and Preact. Since the `urql` and `@urql/preact` packages share most of their API and are
+used in the same way, when reading the documentation on React, all examples are essentially the same,
+except that we'd want to use the `@urql/preact` package instead of the `urql` package.
+
+## Getting started
+
+### Installation
+
+Installing `urql` is as quick as you'd expect and you won't need any other packages to get started
+with at first. We'll install the package with our package manager of choice.
+
+```sh
+yarn add urql graphql
+# or
+npm install --save urql graphql
+```
+
+To use `urql` with Preact, we have to install `@urql/preact` instead of `urql` and import from
+that package instead. Otherwise all examples for Preact will be the same.
+
+Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
+dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
+to install `graphql` alongside `urql`.
+
+Both the `urql` and `graphql` packages follow [semantic versioning](https://semver.org) and all
+`urql` packages will define a range of compatible versions of `graphql`. Watch out for breaking
+changes in the future however, in which case your package manager may warn you about `graphql` being
+out of the defined peer dependency range.
+
+### Setting up the `Client`
+
+The `urql` and `@urql/preact` packages export a method called `createClient` which we can use to
+create the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
+
+```js
+import { createClient } from 'urql';
+
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+});
+```
+
+At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
+
+Another common option is `fetchOptions`. This option allows us to customize the options that will be
+passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
+a function returning an options object.
+
+In the following example we'll add a token to each `fetch` request that our `Client` sends to our
+GraphQL API.
+
+```js
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+  fetchOptions: () => {
+    const token = getToken();
+    return {
+      headers: { authorization: token ? `Bearer ${token}` : '' },
+    };
+  },
+});
+```
+
+### Providing the `Client`
+
+To make use of the `Client` in React & Preact we will have to provide it via the
+[Context API](https://reactjs.org/docs/context.html). This may be done with the help of
+the `Provider` export.
+
+```jsx
+import { createClient, Provider } from 'urql';
+
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+});
+
+const App = () => (
+  <Provider value={client}>
+    <YourRoutes />
+  </Provider>
+);
+```
+
+Now every component and element inside and under the `Provider` are able to use GraphQL queries that
+will be sent to our API.
+
+## Queries
+
+Both libraries offer a `useQuery` hook and a `Query` component. The latter accepts the same
+parameters but we won't cover it in this guide. [Look it up in the API docs if you prefer
+render-props components.](../api/urql.md#query-component)
+
+### Run a first query
+
+For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
+todo items. Let's dive right into it!
+
+```jsx
+import { useQuery } from 'urql';
+
+const TodosQuery = `
+  query {
+    todos {
+      id
+      title
+    }
+  }
+`;
+
+const Todos = () => {
+  const [result, reexecuteQuery] = useQuery({
+    query: TodosQuery,
+  });
+
+  const { data, fetching, error } = result;
+
+  if (fetching) return <p>Loading...</p>;
+  if (error) return <p>Oh no... {error.message}</p>;
+
+  return (
+    <ul>
+      {data.todos.map(todo => (
+        <li key={todo.id}>{todo.title}</li>
+      ))}
+    </ul>
+  );
+};
+```
+
+Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts
+options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The
+tuple we then get in return is an array that contains a result object and a re-execute function.
+
+The result object contains several properties. The `fetching` field indicates whether we're currently
+loading data, `data` contains the actual `data` from the API's result, and `error` is set when either
+the request to the API has failed or when our API result contained some `GraphQLError`s, which
+we'll get into later on the ["Errors" page](../concepts/errors.md).
+
+### Variables
+
+Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
+pagination. For this purpose the `useQuery` hook also accepts a `variables` option, which we can use
+to supply variables to our query.
+
+```jsx
+const TodosListQuery = `
+  query ($from: Int!, $limit: Int!) {
+    todos (from: $from, limit: $limit) {
+      id
+      title
+    }
+  }
+`;
+
+const Todos = ({ from, limit }) => {
+  const [result, reexecuteQuery] = useQuery({
+    query: TodosListQuery,
+    variables: { from, limit },
+  });
+
+  // ...
+};
+```
+
+As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
+`POST` request body that is sent to our GraphQL API.
+
+Whenever the `variables` (or the `query`) option on the `useQuery` hook changes `fetching` will
+switch to `true` and a new request will be sent to our API, unless a result has already been cached
+previously.
+
+### Pausing `useQuery`
+
+In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not
+execute the query otherwise. For instance, we may be building a form and want a validation query to
+only take place when a field has been filled out.
+
+Since hooks in React can't just be commented out, the `useQuery` hook accepts a `pause` option that
+temporarily _freezes_ all changes and stops requests.
+
+In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit`
+variables have been defined to be non-nullable `Int!` values.
+
+Let's pause the query we've just
+written to not execute when these variables are empty, to prevent `null` variables from being
+executed. We can do this by means of setting the `pause` option to `true`:
+
+```jsx
+const Todos = ({ from, limit }) => {
+  const [result, reexecuteQuery] = useQuery({
+    query: TodosListQuery,
+    variables: { from, limit },
+    pause: !from || !limit,
+  });
+
+  // ...
+};
+```
+
+Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed.
+This also means that `result.data` won't change, which means we'll still have access to our old data
+even though the variables may have changed.
+
+### Request Policies
+
+As has become clear in the previous sections of this page, the `useQuery` hook accepts more options
+than just `query` and `variables`. Another option we should touch on is `requestPolicy`.
+
+The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
+default this is set to `cache-first`, which means that we prefer to get results from our cache, but
+are falling back to sending an API request.
+
+Request policies aren't specific to `urql`'s React API, but are a common feature in its core. [You
+can learn more about how the cache behaves given the four different policies on the "Document
+Caching" page.](../concepts/document-caching.md)
+
+### Reexecuting Queries
+
+The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or
+`variables` change, but in some cases we may find that we need to programmatically trigger a new
+query. This is the purpose of the `reexecuteQuery` function which is the second item in the tuple
+that `useQuery` returns.
+
+Triggering a query programmatically may be useful in a couple of cases. It can for instance be used
+to refresh data that is currently being displayed. In these cases we may also override the
+`requestPolicy` of our query just once and set it to `network-only` to skip the cache.
+
+```jsx
+const Todos = ({ from, limit }) => {
+  const [result, reexecuteQuery] = useQuery({
+    query: TodosListQuery,
+    variables: { from, limit },
+  });
+
+  const refresh = () => {
+    // Refetch the query and skip the cache
+    reexecuteQuery({ requestPolicy: 'network-only' });
+  };
+};
+```
+
+Calling `refresh` in the above example will execute the query again forcefully, and will skip the
+cache, since we're passing `requestPolicy: 'network-only'`.
+
+Furthermore the `reexecuteQuery` function can also be used to programmatically start a query even
+when `pause` is set to `true`, which would usually stop all automatic queries.
+
+There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
+it.](../api/urql.md#usequery)
+
+## Mutations
+
+Both libraries offer a `useMutation` hook and a `Mutation` component. The latter accepts the same
+parameters but we won't cover it in this guide. [Look it up in the API docs if you prefer
+render-props components.](../api/urql.md#mutation-component)
+
+### Sending a mutation
+
+Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
+example! We'll set up a mutation that _updates_ a todo item's title.
+
+```jsx
+const UpdateTodo = `
+  mutation ($id: ID!, $title: String!) {
+    updateTodo (id: $id, title: $title) {
+      id
+      title
+    }
+  }
+`;
+
+const Todo = ({ id, title }) => {
+  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
+};
+```
+
+Similar to the `useQuery` output, `useMutation` returns a tuple. The first item in the tuple again
+contains `fetching`, `error`, and `data` — it's identical since this is a common pattern of how
+`urql` presents [operation results](../api/core.md#operationresult).
+
+Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in
+our example, no mutation will be performed. To execute our mutation we instead have to call the
+execute function — `updateTodo` in our example — which is the second item in the tuple.
+
+### Using the mutation result
+
+When calling our `updateTodo` function we have two ways of getting to the result as it comes back
+from our API. We can either use the first value of the returned tuple — our `updateTodoResult` — or
+we can use the promise that `updateTodo` returns.
+
+```jsx
+const Todo = ({ id, title }) => {
+  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
+
+  const submit = newTitle => {
+    const variables = { id, title: newTitle || '' };
+    updateTodo(variables).then(result => {
+      // The result is almost identical to `updateTodoResult` with the exception
+      // of `result.fetching` not being set.
+      // It is an OperationResult.
+    });
+  };
+};
+```
+
+The result is useful when your UI has to display progress on the mutation, and the returned
+promise is particularly useful when you're adding side-effects that run after the mutation has
+completed.
+
+### Handling mutation errors
+
+It's worth noting that the promise we receive when calling the execute function will never
+reject. Instead it will always return a promise that resolves to a result.
+
+If you're checking for errors, you should use `result.error` instead, which will be set
+to a `CombinedError` when any kind of errors occurred while executing your mutation.
+[Read more about errors on our "Errors" page.](../concepts/errors.md)
+
+```jsx
+const Todo = ({ id, title }) => {
+  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
+
+  const submit = newTitle => {
+    const variables = { id, title: newTitle || '' };
+    updateTodo(variables).then(result => {
+      if (result.error) {
+        console.error('Oh no!', result.error);
+      }
+    });
+  };
+};
+```
+
+There are some more tricks we can use with `useMutation`.<br />
+[Read more about its API in the API docs for it.](../api/urql.md#usemutation)
+
+## Reading on
+
+This concludes the introduction for using `urql` with React or Preact. The rest of the documentation
+is mostly framework-agnostic and will apply to either `urql` in general or the `@urql/core` package,
+which is the same between all framework bindings. Hence, next we may want to learn more about one of
+the following to learn more about the internals:
+
+- [How does the default "document cache" work?](../concepts/document-caching.md)
+- [How are errors handled and represented?](../concepts/errors.md)
+- [A quick overview of `urql`'s philosophy and structure.](../concepts/philosophy.md)
+- [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md)
diff --git a/docs/basics/svelte.md b/docs/basics/svelte.md
new file mode 100644
index 00000000..be7bd654
--- /dev/null
+++ b/docs/basics/svelte.md
@@ -0,0 +1,483 @@
+---
+title: Svelte
+order: 2
+---
+
+# Svelte
+
+## Getting started
+
+This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for
+Svelte. The `@urql/svelte` package, which provides bindings for Svelte, doesn't fundamentally
+function differently from `@urql/preact` or `urql` and uses the same [Core Package and
+`Client`](../concepts/core-package.md).
+
+### Installation
+
+Installing `@urql/svelte` is quick and no other packages are immediately necessary.
+
+```sh
+yarn add @urql/svelte graphql
+# or
+npm install --save @urql/svelte graphql
+```
+
+Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
+dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
+to install `graphql` alongside `@urql/svelte`.
+
+Both the `@urql/svelte` and `graphql` packages follow [semantic versioning](https://semver.org) and
+all `@urql/svelte` packages will define a range of compatible versions of `graphql`. Watch out
+for breaking changes in the future however, in which case your package manager may warn you about
+`graphql` being out of the defined peer dependency range.
+
+### Setting up the `Client`
+
+The `@urql/svelte` package exports a method called `createClient` which we can use to create
+the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
+
+```js
+import { createClient } from '@urql/svelte';
+
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+});
+```
+
+At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
+
+Another common option is `fetchOptions`. This option allows us to customize the options that will be
+passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
+a function returning an options object.
+
+In the following example we'll add a token to each `fetch` request that our `Client` sends to our
+GraphQL API.
+
+```js
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+  fetchOptions: () => {
+    const token = getToken();
+    return {
+      headers: { authorization: token ? `Bearer ${token}` : '' },
+    };
+  },
+});
+```
+
+### Providing the `Client`
+
+To make use of the `Client` in Svelte we will have to provide it via the
+[Context API](https://svelte.dev/tutorial/context-api). From a parent component to its child
+components. This will share one `Client` with the rest of our app, if we for instance provide the
+`Client`
+
+```html
+<script>
+  import { createClient, setClient } from '@urql/svelte';
+
+  const client = createClient({
+    url: 'http://localhost:3000/graphql',
+  });
+
+  setClient(client);
+</script>
+```
+
+The `setClient` method internally calls [Svelte's `setContext`
+function](https://svelte.dev/docs#setContext). The `@urql/svelte` package also exposes a `getClient`
+function that uses [`getContext`](https://svelte.dev/docs#getContext) to retrieve the `Client` in
+child components. This is used throughout `@urql/svelte`'s API.
+
+We can also use a convenience function, `initClient`. This function combines the `createClient` and
+`setClient` calls into one.
+
+```html
+<script>
+  import { initClient } from '@urql/svelte';
+
+  initClient({
+    url: 'http://localhost:3000/graphql',
+  });
+</script>
+```
+
+## Queries
+
+We'll implement queries using the `operationStore` and the `query` function from `@urql/svelte`.
+
+The `operationStore` function creates a [Svelte Writable store](https://svelte.dev/docs#writable).
+You can use it to initialise a data container in `urql`. This store holds on to our query inputs,
+like the GraphQL query and variables, which we can change to launch new queries, and also exposes
+the query's eventual result, which we can then observe.
+
+### Run a first query
+
+For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
+todo items. Let's dive right into it!
+
+```jsx
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query {
+      todos {
+        id
+        title
+      }
+    }
+  `);
+
+  query(todos);
+</script>
+
+{#if $todos.fetching}
+<p>Loading...</p>
+{:else if $todos.error}
+<p>Oh no... {$todos.error.message}</p>
+{:else}
+<ul>
+  {#each $todos.data.todos as todo}
+  <li>{todo.title}</li>
+  {/each}
+</ul>
+{/if}
+```
+
+Here we have implemented our first GraphQL query to fetch todos. We're first creating an
+`operationStore` which holds on to our `query` and are then passing the store to the `query`
+function, which starts the GraphQL query.
+
+The `todos` store can now be used like any other Svelte store using a
+[reactive auto-subscription](https://svelte.dev/tutorial/auto-subscriptions) in Svelte. This means
+that we prefix `$todos` with a dollar symbol, which automatically subscribes us to its changes.
+
+The `query` function accepts our store and starts using the `Client` to execute our query. It may
+only be called once for a store and lives alongside the component's lifecycle. It will automatically
+read changes on the `operationStore` and will update our query and results accordingly.
+
+### Variables
+
+Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
+pagination. For this purpose the `operationStore` also accepts a `variables` argument, which we can
+use to supply variables to our query.
+
+```jsx
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    }`,
+    { from, limit }
+  );
+
+  query(todos);
+</script>
+
+...
+```
+
+As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
+`POST` request body that is sent to our GraphQL API.
+
+The `operationStore` also supports being actively changed. This will hook into Svelte's reactivity
+model as well and cause the `query` utility to start a new operation.
+
+```jsx
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    }`,
+    { from, limit }
+  );
+
+  query(todos);
+
+  function nextPage() {
+    $todos.variables.from += $todos.variables.limit;
+  }
+</script>
+
+<button on:click={nextPage}>Next page<button></button></button>
+```
+
+The `operationStore` provides getters too so it's also possible for us to pass `todos` around and
+update `todos.variables` or `todos.query` directly. Both, updating `todos.variables` and
+`$todos.variables` in a component for instance, will cause `query` to pick up the update and execute
+our changes.
+
+### Pausing Queries
+
+In some cases we may want our queries to not execute until a pre-condition has been met. Since the
+`query` operation exists for the entire component lifecycle however, it can't just be stopped and
+started at will. Instead, the `query`'s third argument, the `context`, may have an added `pause`
+option that can be set to `true` to temporarily _freeze_ all changes and stop requests.
+
+For instance we may start out with a paused store and then unpause it once a callback is invoked:
+
+```html
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todo = operationStore(
+    `
+    query {
+      todo {
+        id
+        title
+      }
+    }`,
+    undefined,
+    { pause: true }
+  );
+
+  query(todo);
+
+  function unpause() {
+    $todo.context.pause = false;
+  }
+</script>
+
+<button on:click="{unpause}">Unpause</button>
+```
+
+### Request Policies
+
+The `operationStore` also accepts another argument apart from `query` and `variables`. Optionally
+you may pass a third argument, [the `context` object](../api/core.md#operationcontext). The arguably
+most interesting option the `context` may contain is `requestPolicy`.
+
+The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
+default this is set to `cache-first`, which means that we prefer to get results from our cache, but
+are falling back to sending an API request.
+
+In total there are four different policies that we can use:
+
+- `cache-first` (the default) prefers cached results and falls back to sending an API request when
+  no prior result is cached.
+- `cache-and-network` returns cached results but also always sends an API request, which is perfect
+  for displaying data quickly while keeping it up-to-date.
+- `network-only` will always send an API request and will ignore cached results.
+- `cache-only` will always return cached results or `null`.
+
+The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
+if it has been cached, but also refreshes data in our cache in the background. This means though
+that `fetching` will be `false` for cached results although an API request may still be ongoing in
+the background.
+
+For this reason there's another field on results, `result.stale`, which indicates that the cached
+result is either outdated or that another request is being sent in the background.
+
+```jsx
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    }`,
+    { from, limit },
+    { requestPolicy: 'cache-and-network' }
+  );
+
+  query(todos);
+</script>
+
+...
+```
+
+As we can see, the `requestPolicy` is easily changed here and we can read our `context` option back
+from `todos.context`, just as we can check `todos.query` and `todos.variables`. Updating
+`operationStore.context` can be very useful to also refetch queries, as we'll see in the next
+section.
+
+[You can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
+
+### Reexecuting Queries
+
+The default caching approach in `@urql/svelte` typically takes care of updating queries on the fly
+quite well and does so automatically. Sometimes it may be necessary though to refetch data and to
+execute a query with a different `context`. Triggering a query programmatically may be useful in a
+couple of cases. It can for instance be used to refresh data that is currently being displayed.
+
+We can trigger a new query update by changing out the `context` of our `operationStore`.
+
+```jsx
+<script>
+  import { operationStore, query } from '@urql/svelte';
+
+  const todos = operationStore(`
+    query ($from: Int!, $limit: Int!) {
+      todos(from: $from, limit: $limit) {
+        id
+        title
+      }
+    }`,
+    { from, limit },
+    { requestPolicy: 'cache-first' }
+  );
+
+  query(todos);
+
+  function refresh() {
+    $todos.context = { requestPolicy: 'network-only' };
+  }
+</script>
+```
+
+Calling `refresh` in the above example will execute the query again forcefully, and will skip the
+cache, since we're passing `requestPolicy: 'network-only'`.
+
+### Reading on
+
+There are some more tricks we can use with `operationStore`.
+[Read more about its API in the API docs for it.](../api/svelte.md#operationStore)
+
+## Mutations
+
+The `mutation` function isn't dissimilar from the `query` function but is triggered manually and
+can accept a [`GraphQLRequest` object](../api/core.md#graphqlrequest) too while also supporting our
+trusty `operationStore`.
+
+### Sending a mutation
+
+Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
+example! We'll set up a mutation that _updates_ a todo item's title.
+
+```html
+<script>
+  import { mutation } from '@urql/svelte';
+
+  export let id;
+
+  const mutateTodo = mutation({
+    query: `
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `,
+  });
+
+  function updateTodo(newTitle) {
+    mutateTodo({ id, title: newTitle });
+  }
+</script>
+```
+
+This small call to `mutation` accepts a `query` property (besides the `variables` property) and
+returns an execute function. We've wrapped it in an `updateTodo` function to illustrate its usage.
+
+Unlike the `query` function, the `mutation` function doesn't start our mutation automatically.
+Instead, mutations are started programmatically by calling the function they return. This function
+also returns a promise so that we can use the mutation's result.
+
+### Using the mutation result
+
+When calling `mutateTodo` in our previous example, we start the mutation. To use the mutation's
+result we actually have two options instead of one.
+
+The first option is to use the promise that the `mutation`'s execute function returns. This promise
+will resolve to an `operationStore`, which is what we're used to from sending queries. Using this
+store we can then read the mutation's `data` or `error`.
+
+```html
+<script>
+  import { mutation } from '@urql/svelte';
+
+  export let id;
+
+  const mutateTodo = mutation({
+    query: `
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `,
+  });
+
+  function updateTodo(newTitle) {
+    mutateTodo({ id, title: newTitle }).then(result => {
+      // The result is an operationStore again, which will already carry the mutation's result
+      console.log(result.data, result.error);
+    });
+  }
+</script>
+```
+
+Alternatively, we can pass `mutation` an `operationStore` directly. This allows us to use a
+mutation's result in our component's UI more easily, without storing it ourselves.
+
+```html
+<script>
+  import { operationStore, mutation } from '@urql/svelte';
+
+  export let id;
+
+  const updateTodoStore = operationStore(`
+    mutation ($id: ID!, $title: String!) {
+      updateTodo (id: $id, title: $title) {
+        id
+        title
+      }
+    }
+  `);
+
+  const updateTodoMutation = mutation(updateTodoStore);
+
+  function updateTodo(newTitle) {
+    updateTodoMutation({ id, title: newTitle });
+  }
+</script>
+
+{#if $updateTodoStore.data} Todo was updated! {/if}
+```
+
+### Handling mutation errors
+
+It's worth noting that the promise we receive when calling the execute function will never
+reject. Instead it will always return a promise that resolves to an `operationStore`, even if the
+mutation has failed.
+
+If you're checking for errors, you should use `operationStore.error` instead, which will be set
+to a `CombinedError` when any kind of errors occurred while executing your mutation.
+[Read more about errors on our "Errors" page.](../concepts/errors.md)
+
+```jsx
+mutateTodo({ id, title: newTitle }).then(result => {
+  if (result.error) {
+    console.error('Oh no!', result.error);
+  }
+});
+```
+
+## Reading on
+
+This concludes the introduction for using `urql` with Svelte. The rest of the documentation
+is mostly framework-agnostic and will apply to either `urql` in general or the `@urql/core` package,
+which is the same between all framework bindings. Hence, next we may want to learn more about one of
+the following to learn more about the internals:
+
+- [How does the default "document cache" work?](../concepts/document-caching.md)
+- [How are errors handled and represented?](../concepts/errors.md)
+- [A quick overview of `urql`'s philosophy and structure.](../concepts/philosophy.md)
+- [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md)
diff --git a/docs/basics/vue.md b/docs/basics/vue.md
new file mode 100644
index 00000000..911ad579
--- /dev/null
+++ b/docs/basics/vue.md
@@ -0,0 +1,606 @@
+---
+title: Vue
+order: 1
+---
+
+# Vue
+
+## Getting started
+
+The `@urql/vue` bindings have been written with [Vue
+3](https://github.com/vuejs/vue-next/releases/tag/v3.0.0) in mind and use Vue's newer [Composition
+API](https://v3.vuejs.org/guide/composition-api-introduction.html). This gives the `@urql/vue`
+bindings capabilities to be more easily integrated into your existing `setup()` functions.
+
+### Installation
+
+Installing `@urql/vue` is quick and no other packages are immediately necessary.
+
+```sh
+yarn add @urql/vue graphql
+# or
+npm install --save @urql/vue graphql
+```
+
+Most libraries related to GraphQL also need the `graphql` package to be installed as a peer
+dependency, so that they can adapt to your specific versioning requirements. That's why we'll need
+to install `graphql` alongside `@urql/vue`.
+
+Both the `@urql/vue` and `graphql` packages follow [semantic versioning](https://semver.org) and
+all `@urql/vue` packages will define a range of compatible versions of `graphql`. Watch out
+for breaking changes in the future however, in which case your package manager may warn you about
+`graphql` being out of the defined peer dependency range.
+
+### Setting up the `Client`
+
+The `@urql/vue` package exports a method called `createClient` which we can use to create
+the GraphQL client. This central `Client` manages all of our GraphQL requests and results.
+
+```js
+import { createClient } from '@urql/vue';
+
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+});
+```
+
+At the bare minimum we'll need to pass an API's `url` when we create a `Client` to get started.
+
+Another common option is `fetchOptions`. This option allows us to customize the options that will be
+passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or
+a function returning an options object.
+
+In the following example we'll add a token to each `fetch` request that our `Client` sends to our
+GraphQL API.
+
+```js
+const client = createClient({
+  url: 'http://localhost:3000/graphql',
+  fetchOptions: () => {
+    const token = getToken();
+    return {
+      headers: { authorization: token ? `Bearer ${token}` : '' },
+    };
+  },
+});
+```
+
+### Providing the `Client`
+
+To make use of the `Client` in Vue we will have to provide from a parent component to its child
+components. This will share one `Client` with the rest of our app. In `@urql/vue` there are two
+different ways to achieve this.
+
+The first method is to use `@urql/vue`'s `provideClient` function. This must be called in any of
+your parent components and accepts either a `Client` directly or just the options that you'd pass to
+`createClient`.
+
+```html
+<script>
+  import { createClient, provideClient } from '@urql/vue';
+
+  const client = createClient({
+    url: 'http://localhost:3000/graphql',
+  });
+
+  provideClient(client);
+</script>
+```
+
+Alternatively we may use the exported `install` function and treat `@urql/vue` as a plugin by
+importing its default export and using it [as a plugin](https://v3.vuejs.org/guide/plugins.html#using-a-plugin).
+
+```js
+import { createApp } from 'vue';
+import Root from './App.vue';
+import urql from '@urql/vue';
+
+const app = createApp(Root);
+
+app.use(urql, {
+  url: 'http://localhost:3000/graphql',
+});
+
+app.mount('#app');
+```
+
+The plugin also accepts `createClient`'s options or a `Client` as its inputs.
+
+## Queries
+
+We'll implement queries using the `useQuery` function from `@urql/vue`.
+
+### Run a first query
+
+For the following examples, we'll imagine that we're querying data from a GraphQL API that contains
+todo items. Let's dive right into it!
+
+```jsx
+<template>
+  <div v-if="fetching">
+    Loading...
+  </div>
+  <div v-else-if="error">
+    Oh no... {{error}}
+  </div>
+  <div v-else>
+    <ul v-if="data">
+      <li v-for="todo in data.todos">{{ todo.title }}</li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import { useQuery } from '@urql/vue';
+
+export default {
+  setup() {
+    const result = useQuery({
+      query: `
+        {
+          todos {
+            id
+            title
+          }
+        }
+      `
+    });
+
+    return {
+      fetching: result.fetching,
+      data: result.data,
+      error: result.error,
+    };
+  }
+};
+</script>
+```
+
+Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts
+options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The
+tuple we then get in return is an array that contains a result object and a re-execute function.
+
+The result object contains several properties. The `fetching` field indicates whether we're currently
+loading data, `data` contains the actual `data` from the API's result, and `error` is set when either
+the request to the API has failed or when our API result contained some `GraphQLError`s, which
+we'll get into later on the ["Errors" page](../concepts/errors.md).
+
+All of these properties on the result are derived from the [shape of
+`OperationResult`](../api/core.md#operationresult) and are marked as [reactive
+](https://v3.vuejs.org/guide/reactivity-fundamentals.html), which means they may
+update while the query is running, which will automatically update your UI.
+
+### Variables
+
+Typically we'll also need to pass variables to our queries, for instance, if we are dealing with
+pagination. For this purpose `useQuery` also accepts a `variables` input, which we can
+use to supply variables to our query.
+
+```jsx
+<template>
+  ...
+</template>
+
+<script>
+import { useQuery } from '@urql/vue';
+
+export default {
+  props: ['from', 'limit'],
+  setup({ from, limit }) {
+    return useQuery({
+      query: `
+        query ($from: Int!, $limit: Int!) {
+          todos(from: $from, limit: $limit) {
+            id
+            title
+          }
+        }
+      `,
+      variables: { from, limit }
+    });
+  }
+};
+</script>
+```
+
+As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the
+`POST` request body that is sent to our GraphQL API.
+
+All inputs that are passed to `useQuery` may also be [reactive
+state](https://v3.vuejs.org/guide/reactivity-fundamentals.html). This means that both the inputs and
+outputs of `useQuery` are reactive and may change over time.
+
+```jsx
+<template>
+  <ul v-if="data">
+    <li v-for="todo in data.todos">{{ todo.title }}</li>
+  </ul>
+  <button @click="from += 10">Next Page</button>
+</template>
+
+<script>
+import { useQuery } from '@urql/vue';
+
+export default {
+  setup() {
+    const from = ref(0);
+
+    const result = useQuery({
+      query: `
+        query ($from: Int!, $limit: Int!) {
+          todos(from: $from, limit: $limit) {
+            id
+            title
+          }
+        }
+      `,
+      variables: { from, limit: 10 }
+    });
+
+    return {
+      from,
+      data: result.data,
+    };
+  }
+};
+</script>
+```
+
+### Pausing `useQuery`
+
+In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not
+execute the query otherwise. For instance, we may be building a form and want a validation query to
+only take place when a field has been filled out.
+
+Since with Vue 3's Composition API we won't just conditionally call `useQuery` we can instead pass a
+reactive `pause` input to `useQuery`.
+
+In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit`
+variables have been defined to be non-nullable `Int!` values.
+
+Let's pause the query we've just written to not execute when these variables are empty, to
+prevent `null` variables from being executed. We can do this by computing `pause` to become `true`
+whenever these variables are falsy:
+
+```js
+import { reactive } from 'vue'
+import { useQuery } from '@urql/vue';
+
+export default {
+  props: ['from', 'limit'],
+  setup({ from, limit }) {
+    return useQuery({
+      query: `
+        query ($from: Int!, $limit: Int!) {
+          todos(from: $from, limit: $limit) {
+            id
+            title
+          }
+        }
+      `,
+      variables: { from, limit },
+      pause: computed(() => !from.value || !limit.value)
+    });
+  }
+};
+</script>
+```
+
+Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed.
+This also means that `result.data` won't change, which means we'll still have access to our old data
+even though the variables may have changed.
+
+It's worth noting that depending on whether `from` and `limit` are reactive or not you may have to
+change how `pause` is computed. But there's also an imperative alternative to this API. Not only
+does the result you get back from `useQuery` have an `isPaused` ref, it also has `pause()` and
+`resume()` methods.
+
+```jsx
+<template>
+  <div v-if="fetching">
+    Loading...
+  </div>
+  <button @click="isPaused ? resume() : pause()">Toggle Query</button>
+</template>
+
+<script>
+import { useQuery } from '@urql/vue';
+
+export default {
+  setup() {
+    return useQuery({
+      query: `
+        {
+          todos {
+            id
+            title
+          }
+        }
+      `
+    });
+  }
+};
+</script>
+```
+
+This means that no matter whether you're in or outside of `setup()` or rather supplying the inputs
+to `useQuery` or using the outputs, you'll have access to ways to pause or unpause the query.
+
+### Request Policies
+
+As has become clear in the previous sections of this page, the `useQuery` hook accepts more options
+than just `query` and `variables`. Another option we should touch on is `requestPolicy`.
+
+The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By
+default this is set to `cache-first`, which means that we prefer to get results from our cache, but
+are falling back to sending an API request.
+
+In total there are four different policies that we can use:
+
+- `cache-first` (the default) prefers cached results and falls back to sending an API request when
+  no prior result is cached.
+- `cache-and-network` returns cached results but also always sends an API request, which is perfect
+  for displaying data quickly while keeping it up-to-date.
+- `network-only` will always send an API request and will ignore cached results.
+- `cache-only` will always return cached results or `null`.
+
+The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
+if it has been cached, but also refreshes data in our cache in the background. This means though
+that `fetching` will be `false` for cached results although an API request may still be ongoing in
+the background.
+
+For this reason there's another field on results, `result.stale`, which indicates that the cached
+result is either outdated or that another request is being sent in the background.
+
+Request policies aren't specific to `urql`'s Vue bindings, but are a common feature in its core. [You
+can learn more about request policies on the API docs.](../api/core.md#requestpolicy)
+
+### Reexecuting Queries
+
+The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or
+`variables` change, but in some cases we may find that we need to programmatically trigger a new
+query. This is the purpose of the `executeQuery` method which is a method on the result object
+that `useQuery` returns.
+
+Triggering a query programmatically may be useful in a couple of cases. It can for instance be used
+to refresh data that is currently being displayed. In these cases we may also override the
+`requestPolicy` of our query just once and set it to `network-only` to skip the cache.
+
+```js
+import { useQuery } from '@urql/vue';
+
+export default {
+  setup() {
+    const result = useQuery({
+      query: `
+        {
+          todos {
+            id
+            title
+          }
+        }
+      `
+    });
+
+    return {
+      data: result.data,
+      fetching: result.fetching,
+      error: result.error,
+      refresh() {
+        result.executeQuery({
+          requestPolicy: 'network-only'
+        });
+      }
+    };
+  }
+};
+</script>
+```
+
+Calling `refresh` in the above example will execute the query again forcefully, and will skip the
+cache, since we're passing `requestPolicy: 'network-only'`.
+
+Furthermore the `executeQuery` method can also be used to programmatically start a query even
+when `pause` is `true`, which would usually stop all automatic queries.
+
+### Vue Suspense
+
+In Vue 3 a [new feature was introduced](https://vuedose.tips/go-async-in-vue-3-with-suspense/) that
+natively allows components to suspend while data is loading, which works universally on the server
+and on the client, where a replacement loading template is rendered on a parent while data is
+loading.
+
+Any component's `setup()` function can be updated to instead be an `async setup()` function, in
+other words, to return a `Promise` instead of directly returning its data. This means that we can
+update any `setup()` function to make use of Suspense.
+
+The `useQuery`'s returned result supports this, since it is a `PromiseLike`. We can update one of
+our examples to have a suspending component by changing our usage of `useQuery`:
+
+```jsx
+<template>
+  <ul>
+    <li v-for="todo in data.todos">{{ todo.title }}</li>
+  </ul>
+</template>
+
+<script>
+import { useQuery } from '@urql/vue';
+
+export default {
+  async setup() {
+    const { data, error } = await useQuery({
+      query: `
+        {
+          todos {
+            id
+            title
+          }
+        }
+      `
+    });
+
+    return { data };
+  }
+};
+</script>
+```
+
+As we can see, `await useQuery(...)` here suspends the component and what we render will not have to
+handle the loading states of `useQuery` at all. Instead in Vue Suspense we'll have to wrap a parent
+component in a "Suspense boundary." This boundary is what switches a parent to a loading state while
+parts of its children are fetching data. The suspense promise is in essence "bubbling up" until it
+finds a "Suspense boundary".
+
+```
+<template>
+ <Suspense>
+   <template #default>
+     <MyAsyncComponent />
+   </template>
+   <template #fallback>
+     <span>Loading...</span>
+   </template>
+ </Suspense>
+</template>
+```
+
+As long as any parent component is wrapping our component which uses `async setup()` in this
+boundary, we'll get Vue Suspense to work correctly and trigger this loading state. When a child
+suspends this component will switch to using its `#fallback` template rather than its `#default`
+template.
+
+### Reading on
+
+There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
+it.](../api/vue.md#usequery)
+
+## Mutations
+
+The `useMutation` function isn't dissimilar from `useQuery` but is triggered manually and accepts
+only a `DocumentNode` or `string` as an input.
+
+### Sending a mutation
+
+Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an
+example! We'll set up a mutation that _updates_ a todo item's title.
+
+```js
+import { useMutation } from '@urql/vue';
+
+export default {
+  setup() {
+    const { executeMutation: updateTodo } = useMutation(`
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `);
+
+    return { updateTodo };
+  },
+};
+```
+
+Similar to the `useQuery` output, `useMutation` returns a result object, which reflects the data of
+an executed mutation. That means it'll contain the familiar `fetching`, `error`, and `data`
+properties — it's identical since this is a common pattern of how `urql`
+presents [operation results](../api/core.md#operationresult).
+
+Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in
+our example, no mutation will be performed. To execute our mutation we instead have to call the
+`executeMutation` method on the result with some variables.
+
+### Using the mutation result
+
+When calling our `updateTodo` function we have two ways of getting to the result as it comes back
+from our API. We can either use the result itself, since all properties related to the last
+[operation result](../api/core.md#operationresult) are marked as [reactive
+](https://v3.vuejs.org/guide/reactivity-fundamentals.html) — or we can use the promise that the
+`executeMutation` method returns when it's called:
+
+```js
+import { useMutation } from '@urql/vue';
+
+export default {
+  setup() {
+    const updateTodoResult = useMutation(`
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `);
+
+    return {
+      updateTodo(id, title) {
+        const variables = { id, title: title || '' };
+        updateTodoResult(variables).then(result => {
+          // The result is almost identical to `updateTodoResult` with the exception
+          // of `result.fetching` not being set and its properties not being reactive.
+          // It is an OperationResult.
+        });
+      },
+    };
+  },
+};
+```
+
+The reactive result that `useMutation` returns is useful when your UI has to display progress or
+results on the mutation, and the returned promise is particularly useful when you're adding
+side-effects that run after the mutation has completed.
+
+### Handling mutation errors
+
+It's worth noting that the promise we receive when calling the execute function will never
+reject. Instead it will always return a promise that resolves to a result.
+
+If you're checking for errors, you should use `result.error` instead, which will be set
+to a `CombinedError` when any kind of errors occurred while executing your mutation.
+[Read more about errors on our "Errors" page.](../concepts/errors.md)
+
+```js
+import { useMutation } from '@urql/vue';
+
+export default {
+  setup() {
+    const updateTodoResult = useMutation(`
+      mutation ($id: ID!, $title: String!) {
+        updateTodo (id: $id, title: $title) {
+          id
+          title
+        }
+      }
+    `);
+
+    return {
+      updateTodo(id, title) {
+        const variables = { id, title: title || '' };
+        updateTodoResult(variables).then(result => {
+          if (result.error) {
+            console.error('Oh no!', result.error);
+          }
+        });
+      },
+    };
+  },
+};
+```
+
+There are some more tricks we can use with `useMutation`.<br />
+[Read more about its API in the API docs for it.](../api/vue.md#usemutation)
+
+## Reading on
+
+This concludes the introduction for using `urql` with Svelte. The rest of the documentation
+is mostly framework-agnostic and will apply to either `urql` in general or the `@urql/core` package,
+which is the same between all framework bindings. Hence, next we may want to learn more about one of
+the following to learn more about the internals:
+
+- [How does the default "document cache" work?](../concepts/document-caching.md)
+- [How are errors handled and represented?](../concepts/errors.md)
+- [A quick overview of `urql`'s philosophy and structure.](../concepts/philosophy.md)
+- [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md)
diff --git a/docs/concepts/README.md b/docs/concepts/README.md
index 0bb85ec6..7c1ef801 100644
--- a/docs/concepts/README.md
+++ b/docs/concepts/README.md
@@ -10,12 +10,13 @@ inner workings of the `Client`, and how to write extensions and addons, also kno
 
 Each page goes a little further in explaining a core concept of `urql`.
 
+- [**Document Caching**](./document-caching.md) explains the default cache mechanism of `urql`, as opposed to the opt-in
+  [Normalized Cache](../graphcache/normalized-caching.md).
+- [**Errors**](./errors.md) contains information on error handling in `urql`.
 - [**Philosophy**](./philosophy.md) gives a quick overview of the different aspects of GraphQL clients and `urql` in
   particular, which shines a light on why you may want to use `urql`.
 - [**Stream Pattern**](./stream-patterns.md) explains the inner working of `urql`, which is _stream-based_, also known as
   Observable patterns in JS.
-- [**Core Package**](./core-package.md) defines why a shared package exists that contains the main logic of `urql`, and
-  how we can use it directly in Node.js.
 - [**Exchanges**](./exchanges.md) finally introduces _Exchanges_ and how to write extensions or addons and use them
   in `urql`.
 
diff --git a/docs/concepts/core-package.md b/docs/concepts/core-package.md
deleted file mode 100644
index 7b7172e8..00000000
--- a/docs/concepts/core-package.md
+++ /dev/null
@@ -1,104 +0,0 @@
----
-title: Core Package
-order: 2
----
-
-# The Core Package
-
-The `@urql/core` package contains `urql`'s `Client`, some common utilities, and some default
-_Exchanges_. These are the shared, default parts of `urql` that we will be using no matter which
-framework we're interacting with.
-
-Therefore those are also the parts of `urql` that contain its most important logic — like the
-[`Client`](../api/core.md#client) — and the package that we need to know about if we're either integrating `urql` with a new
-framework, or if we're using the "raw" `Client` in Node.js.
-
-## Background
-
-The ["Philosophy"](./philosophy.md) page explains how `urql` solves some of problems encountered when different aspects
-of having a GraphQL client handle declarative querying and being a central point of extensibility.
-
-By extension there are three parts of `urql` you'll come in contact with when you add it to your
-app:
-
-- the framework integration that allows you to declaratively write queries and mutations in your
-  preferred framework, which are currently the [`urql`](../api/urql.md) or
-  [`@urql/preact`](../api/preact.md) packages
-- the [`Client`](../api/core.md#client) that manages the operation lifecycle and results
-- and, exchanges that may either be some default exchanges or some from external packages
-
-On this page we'll learn about the latter two points - shared logic that isn't specific to
-a particular library or framework (such as React or Preact code).
-
-_Exchanges_ are discussed in more detail on the [next page](./exchanges.md).
-
-## Usage with Node.js
-
-The largest part of `urql` itself and the core package is the aforementioned `Client`. It's often
-used directly when using `urql` in Node.js without any other integration.
-
-We've previously seen how we can use the `Client`'s stream methods directly, in [Stream
-Patterns](./stream-patterns.md). However, the [`Client`](../api/core.md#client) also has plenty of
-convenience methods that make interacting with the `Client` directly a lot easier.
-
-### One-off Queries and Mutations
-
-When you're using `urql` to send one-off queries or mutations — rather than in full framework code,
-where updates are important — it's common to convert the streams that we get to promises. The
-`client.query` and `client.mutation` methods have a shortcut to do just that.
-
-```js
-const QUERY = `
-  query Test($id: ID!) {
-    getUser(id: $id) {
-      id
-      name
-    }
-  }
-`;
-
-client
-  .query(QUERY, { id: 'test' })
-  .toPromise()
-  .then(result => {
-    console.log(result); // { data: ... }
-  });
-```
-
-This may be useful when we don't plan on cancelling queries or we don't care about future updates to
-this data and are just looking to query a result once.
-
-Similarly there's a way to read data from the cache synchronously, provided that the cache has
-received a result for a given query before. The `Client` has a `readQuery` method which is a
-shortcut for just that.
-
-```js
-const QUERY = `
-  query Test($id: ID!) {
-    getUser(id: $id) {
-      id
-      name
-    }
-  }
-`;
-
-const result = client.readQuery(QUERY, { id: 'test' });
-
-result; // null or { data: ... }
-```
-
-Since the streams in `urql` operate synchronously, internally this method subscribes to
-`client.executeQuery` and unsubscribes immediately. If a result is available in the cache it will be
-resolved synchronously prior to the unsubscribe. If not, the query is cancelled and no request will be sent to the GraphQL API.
-
-## Common Utilities in Core
-
-The `@urql/core` package contains other utilities that are shared between multiple addon packages.
-This is a short but non-exhaustive list. It contains,
-
-- [`CombinedError`](../api/core.md#combinederror) - our abstraction to combine one or more `GraphQLError`(s) and a `NetworkError`
-- `makeResult` and `makeErrorResult` - utilities to create _Operation Results_
-- [`createRequest`](../api/core.md#createrequest) - a utility function to create a request from a query and some variables (which
-  generates a stable _Operation Key_)
-
-There are other utilities not mentioned here. Read more about the `@urql/core` API in the [API docs](../api/core.md).
diff --git a/docs/basics/document-caching.md b/docs/concepts/document-caching.md
similarity index 77%
rename from docs/basics/document-caching.md
rename to docs/concepts/document-caching.md
index 42b5564c..6feed2fd 100644
--- a/docs/basics/document-caching.md
+++ b/docs/concepts/document-caching.md
@@ -1,6 +1,6 @@
 ---
 title: Document Caching
-order: 3
+order: 1
 ---
 
 # Document Caching
@@ -39,11 +39,24 @@ while it doesn't deal with normalized data or IDs.
 
 ## Request Policies
 
-[We previously covered request policies on the "Queries" page.](./queries.md)
-
 The _request policy_ that is defined will alter what the default document cache does. By default the
-cache will prefer cached results and will otherwise send a request, which is called `cache-first`,
-but there's also `cache-and-network`, `cache-only`, and `network-only`.
+cache will prefer cached results and will otherwise send a request, which is called `cache-first`.
+In total there are four different policies that we can use:
+
+- `cache-first` (the default) prefers cached results and falls back to sending an API request when
+  no prior result is cached.
+- `cache-and-network` returns cached results but also always sends an API request, which is perfect
+  for displaying data quickly while keeping it up-to-date.
+- `network-only` will always send an API request and will ignore cached results.
+- `cache-only` will always return cached results or `null`.
+
+The `cache-and-network` policy is particularly useful, since it allows us to display data instantly
+if it has been cached, but also refreshes data in our cache in the background. This means though
+that `fetching` will be `false` for cached results although an API request may still be ongoing in
+the background.
+
+For this reason there's another field on results, `result.stale`, which indicates that the cached
+result is either outdated or that another request is being sent in the background.
 
 [Read more about which request policies are available in the API
 docs.](../api/core.md#requestpolicy-type)
diff --git a/docs/basics/errors.md b/docs/concepts/errors.md
similarity index 99%
rename from docs/basics/errors.md
rename to docs/concepts/errors.md
index aeb7b6d0..2d892541 100644
--- a/docs/basics/errors.md
+++ b/docs/concepts/errors.md
@@ -1,6 +1,6 @@
 ---
 title: Errors
-order: 4
+order: 2
 ---
 
 # Error handling
diff --git a/docs/concepts/exchanges.md b/docs/concepts/exchanges.md
index ddbb2a95..ab657206 100644
--- a/docs/concepts/exchanges.md
+++ b/docs/concepts/exchanges.md
@@ -1,6 +1,6 @@
 ---
 title: Exchanges
-order: 3
+order: 5
 ---
 
 # Exchanges
@@ -17,7 +17,7 @@ fulfill our GraphQL requests and can change the stream of operations or results.
 The default set of exchanges that `@urql/core` contains and applies to a `Client` are:
 
 - `dedupExchange`: Deduplicates pending operations (pending = waiting for a result)
-- `cacheExchange`: The default caching logic with ["Document Caching"](../basics/document-caching.md)
+- `cacheExchange`: The default caching logic with ["Document Caching"](../concepts/document-caching.md)
 - `fetchExchange`: Sends an operation to the API using `fetch` and adds results to the output stream
 
 Other available exchanges:
diff --git a/docs/concepts/philosophy.md b/docs/concepts/philosophy.md
index 6e2058c1..b75a3aec 100644
--- a/docs/concepts/philosophy.md
+++ b/docs/concepts/philosophy.md
@@ -1,6 +1,6 @@
 ---
 title: Philosophy
-order: 1
+order: 3
 ---
 
 # Philosophy
@@ -18,9 +18,6 @@ In this guide, we will walk through how `urql` is set up internally and how all
 
 ## Hello World
 
-We previously read about how to set up a `Client` in [Getting
-Started](../basics/getting-started.md).
-
 When you use `urql` you will always create and set up a `Client`. There is a `createClient`
 convenience helper to do just that.
 
@@ -81,7 +78,7 @@ cache the results of queries automatically. In `urql` the `Client` can be extend
 cache implementations; all of them allow you to prevent mixing your declarative query or mutation
 code with cache-implementation details, as they mostly happen behind the scenes.
 
-We previously read about the default [Document Caching](../basics/document-caching.md).
+We previously read about the default [Document Caching](../concepts/document-caching.md).
 
 Some GraphQL clients also resort to caching data in a normalized format. This is similar to
 [how you may store data in Redux.](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape/)
diff --git a/docs/concepts/stream-patterns.md b/docs/concepts/stream-patterns.md
index f0bc2ba8..16b4f9ec 100644
--- a/docs/concepts/stream-patterns.md
+++ b/docs/concepts/stream-patterns.md
@@ -1,6 +1,6 @@
 ---
 title: Stream Patterns
-order: 1
+order: 4
 ---
 
 # Stream Patterns
diff --git a/docs/graphcache/README.md b/docs/graphcache/README.md
index 71eb15bd..48727e6f 100644
--- a/docs/graphcache/README.md
+++ b/docs/graphcache/README.md
@@ -6,7 +6,7 @@ order: 5
 # Graphcache
 
 In `urql`, caching is fully configurable via [_Exchanges_](../concepts/exchanges.md) and the default
-`cacheExchange` in `urql` offers a ["Document Cache"](../basics/document-caching.md), which is
+`cacheExchange` in `urql` offers a ["Document Cache"](../concepts/document-caching.md), which is
 sufficient for sites that heavily rely and render static content. However as an app grows more
 complex it's likely that the data and state that `urql` manages, will also grow more complex and
 introduce interdependencies between data.
@@ -21,7 +21,7 @@ In `urql`, normalized caching is an opt-in feature which is provided by the
 ## Features
 
 The following pages introduce different features in _Graphcache_ which together make it a compelling
-alternative to the standard [document cache](../basics/document-caching.md) that `urql` uses by
+alternative to the standard [document cache](../concepts/document-caching.md) that `urql` uses by
 default.
 
 - 🔁 [**Fully reactive, normalized caching.**](./normalized-caching.md) _Graphcache_ stores data in
diff --git a/docs/graphcache/normalized-caching.md b/docs/graphcache/normalized-caching.md
index fc62df59..8279273b 100644
--- a/docs/graphcache/normalized-caching.md
+++ b/docs/graphcache/normalized-caching.md
@@ -12,7 +12,7 @@ even lists of entities, which we call "links". The scalar fields on entities lik
 etc is what we call "records."
 
 Instead of storing query results as whole documents, like `urql` does with [its default "Document
-Caching"](../basics/document-caching.md), _Graphcache_ flattens all data it receives automatically.
+Caching"](../concepts/document-caching.md), _Graphcache_ flattens all data it receives automatically.
 If we looked at doing this manually on the following piece of data, we'd separate each object into a
 list of key-value entries per entity.
 
diff --git a/docs/graphcache/offline.md b/docs/graphcache/offline.md
index f6bccd1a..45c0e81c 100644
--- a/docs/graphcache/offline.md
+++ b/docs/graphcache/offline.md
@@ -102,7 +102,7 @@ offline — either because `navigator.onLine` is `false` or because the error me
 offline error — the `offlineExchange` won't deliver the error for this query to avoid it from being
 surfaced to the user. This works particularly well in combination with ["Schema
 Awareness"](./schema-awareness.md) which will deliver as much of a partial query result as possible.
-In combination with the [`cache-and-network` request policy](../basics/queries.md#request-policies)
+In combination with the [`cache-and-network` request policy](../concepts/document-caching.md#request-policies)
 we can now ensure that we display as much data as possible when the user is offline while still
 keeping the cache up-to-date when the user is online.
 
diff --git a/docs/graphcache/under-the-hood.md b/docs/graphcache/under-the-hood.md
index e5acb1a9..f8847421 100644
--- a/docs/graphcache/under-the-hood.md
+++ b/docs/graphcache/under-the-hood.md
@@ -28,7 +28,7 @@ This layering approach means that Graphcache is able to temporarily reorder resu
 
 We've previously learned about [how to create "Optimistic Updates" in an earlier section.](./custom-updates.md#optimistic-updates)
 Optimistic updates can temporarily update cached data after incoming mutations, which will trigger on screen queries to update.
-However, if we also use [the `cache-and-network` request policy](../basics/queries.md#request-policies) at the same time, some queries can refetch and overwrite our optimistic data,
+However, if we also use [the `cache-and-network` request policy](../concepts/document-caching.md#request-policies) at the same time, some queries can refetch and overwrite our optimistic data,
 causing an unintended state where the intended optimistic update is destroyed.
 Such an unintended refetch can also happen if after an optimistic update a query is refetched when it’s not or
 only partially available in the cache.
diff --git a/exchanges/graphcache/src/cacheExchange.ts b/exchanges/graphcache/src/cacheExchange.ts
index 906f06f0..3773e933 100644
--- a/exchanges/graphcache/src/cacheExchange.ts
+++ b/exchanges/graphcache/src/cacheExchange.ts
@@ -225,11 +225,22 @@ export const cacheExchange = (opts?: CacheExchangeOpts): Exchange => ({
     if (result.data) {
       // Write the result to cache and collect all dependencies that need to be
       // updated
-      const writeDependencies = write(store, operation, result.data, key)
-        .dependencies;
+      const writeDependencies = write(
+        store,
+        operation,
+        result.data,
+        result.error,
+        key
+      ).dependencies;
       collectPendingOperations(pendingOperations, writeDependencies);
 
-      const queryResult = query(store, operation, result.data, key);
+      const queryResult = query(
+        store,
+        operation,
+        result.data,
+        result.error,
+        key
+      );
       result.data = queryResult.data;
       if (operation.kind === 'query') {
         // Collect the query's dependencies for future pending operation updates
diff --git a/exchanges/graphcache/src/operations/query.ts b/exchanges/graphcache/src/operations/query.ts
index 6d811a79..cf0085ed 100644
--- a/exchanges/graphcache/src/operations/query.ts
+++ b/exchanges/graphcache/src/operations/query.ts
@@ -1,4 +1,5 @@
 import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql';
+import { CombinedError } from '@urql/core';
 
 import {
   getSelectionSet,
@@ -6,9 +7,6 @@ import {
   SelectionSet,
   getFragmentTypeName,
   getFieldAlias,
-} from '../ast';
-
-import {
   getFragments,
   getMainOperation,
   normalizeVariables,
@@ -44,6 +42,7 @@ import {
   ensureData,
   makeContext,
   updateContext,
+  getFieldError,
 } from './shared';
 
 import {
@@ -62,10 +61,11 @@ export const query = (
   store: Store,
   request: OperationRequest,
   data?: Data,
+  error?: CombinedError | undefined,
   key?: number
 ): QueryResult => {
   initDataState('read', store.data, (data && key) || null);
-  const result = read(store, request, data);
+  const result = read(store, request, data, error);
   clearDataState();
   return result;
 };
@@ -73,7 +73,8 @@ export const query = (
 export const read = (
   store: Store,
   request: OperationRequest,
-  input?: Data
+  input?: Data,
+  error?: CombinedError | undefined
 ): QueryResult => {
   const operation = getMainOperation(request.query);
   const rootKey = store.rootFields[operation.operation];
@@ -84,7 +85,9 @@ export const read = (
     normalizeVariables(operation, request.variables),
     getFragments(request.query),
     rootKey,
-    rootKey
+    rootKey,
+    false,
+    error
   );
 
   if (process.env.NODE_ENV !== 'production') {
@@ -117,24 +120,31 @@ const readRoot = (
   select: SelectionSet,
   originalData: Data
 ): Data => {
-  if (typeof originalData.__typename !== 'string') {
+  const typename = ctx.store.rootNames[entityKey]
+    ? entityKey
+    : originalData.__typename;
+  if (typeof typename !== 'string') {
     return originalData;
   }
 
   const iterate = makeSelectionIterator(entityKey, entityKey, select, ctx);
-  const data = {} as Data;
-  data.__typename = originalData.__typename;
+  const data = { __typename: typename };
 
   let node: FieldNode | void;
   while ((node = iterate())) {
     const fieldAlias = getFieldAlias(node);
     const fieldValue = originalData[fieldAlias];
+    // Add the current alias to the walked path before processing the field's value
+    ctx.__internal.path.push(fieldAlias);
+    // Process the root field's value
     if (node.selectionSet && fieldValue !== null) {
       const fieldData = ensureData(fieldValue);
       data[fieldAlias] = readRootField(ctx, getSelectionSet(node), fieldData);
     } else {
       data[fieldAlias] = fieldValue;
     }
+    // After processing the field, remove the current alias from the path again
+    ctx.__internal.path.pop();
   }
 
   return data;
@@ -147,8 +157,15 @@ const readRootField = (
 ): Data | NullArray<Data> | null => {
   if (Array.isArray(originalData)) {
     const newData = new Array(originalData.length);
-    for (let i = 0, l = originalData.length; i < l; i++)
+    for (let i = 0, l = originalData.length; i < l; i++) {
+      // Add the current index to the walked path before reading the field's value
+      ctx.__internal.path.push(i);
+      // Recursively read the root field's value
       newData[i] = readRootField(ctx, select, originalData[i]);
+      // After processing the field, remove the current index from the path
+      ctx.__internal.path.pop();
+    }
+
     return newData;
   } else if (originalData === null) {
     return null;
@@ -289,14 +306,19 @@ const readSelection = (
       isFieldAvailableOnType(store.schema, typename, fieldName);
     }
 
+    // We directly assign typenames and skip the field afterwards
+    if (fieldName === '__typename') {
+      data[fieldAlias] = typename;
+      continue;
+    }
+
     // We temporarily store the data field in here, but undefined
     // means that the value is missing from the cache
     let dataFieldValue: void | DataField;
+    // Add the current alias to the walked path before processing the field's value
+    ctx.__internal.path.push(fieldAlias);
 
-    if (fieldName === '__typename') {
-      data[fieldAlias] = typename;
-      continue;
-    } else if (resultValue !== undefined && node.selectionSet === undefined) {
+    if (resultValue !== undefined && node.selectionSet === undefined) {
       // The field is a scalar and can be retrieved directly from the result
       dataFieldValue = resultValue;
     } else if (
@@ -377,6 +399,15 @@ const readSelection = (
       }
     }
 
+    // If we have an error registered for the current field change undefined values to null
+    if (dataFieldValue === undefined && !!getFieldError(ctx)) {
+      hasPartials = true;
+      dataFieldValue = null;
+    }
+
+    // After processing the field, remove the current alias from the path again
+    ctx.__internal.path.pop();
+
     // Now that dataFieldValue has been retrieved it'll be set on data
     // If it's uncached (undefined) but nullable we can continue assembling
     // a partial query result
@@ -420,6 +451,8 @@ const resolveResolverResult = (
       !store.schema || isListNullable(store.schema, typename, fieldName);
     const data = new Array(result.length);
     for (let i = 0, l = result.length; i < l; i++) {
+      // Add the current index to the walked path before reading the field's value
+      ctx.__internal.path.push(i);
       // Recursively read resolver result
       const childResult = resolveResolverResult(
         ctx,
@@ -431,7 +464,9 @@ const resolveResolverResult = (
         prevData != null ? prevData[i] : undefined,
         result[i]
       );
-
+      // After processing the field, remove the current index from the path
+      ctx.__internal.path.pop();
+      // Check the result for cache-missed values
       if (childResult === undefined && !_isListNullable) {
         return undefined;
       } else {
@@ -478,6 +513,9 @@ const resolveLink = (
       store.schema && isListNullable(store.schema, typename, fieldName);
     const newLink = new Array(link.length);
     for (let i = 0, l = link.length; i < l; i++) {
+      // Add the current index to the walked path before reading the field's value
+      ctx.__internal.path.push(i);
+      // Recursively read the link
       const childLink = resolveLink(
         ctx,
         link[i],
@@ -486,6 +524,9 @@ const resolveLink = (
         select,
         prevData != null ? prevData[i] : undefined
       );
+      // After processing the field, remove the current index from the path
+      ctx.__internal.path.pop();
+      // Check the result for cache-missed values
       if (childLink === undefined && !_isListNullable) {
         return undefined;
       } else {
diff --git a/exchanges/graphcache/src/operations/shared.ts b/exchanges/graphcache/src/operations/shared.ts
index 8a0aa757..d4b79e08 100644
--- a/exchanges/graphcache/src/operations/shared.ts
+++ b/exchanges/graphcache/src/operations/shared.ts
@@ -1,4 +1,10 @@
-import { FieldNode, InlineFragmentNode, FragmentDefinitionNode } from 'graphql';
+import { CombinedError } from '@urql/core';
+import {
+  GraphQLError,
+  FieldNode,
+  InlineFragmentNode,
+  FragmentDefinitionNode,
+} from 'graphql';
 
 import {
   isInlineFragment,
@@ -24,31 +30,63 @@ export interface Context {
   parentFieldKey: string;
   parent: Data;
   fieldName: string;
+  error: GraphQLError | undefined;
   partial: boolean;
   optimistic: boolean;
+  __internal: {
+    path: Array<string | number>;
+    errorMap: { [path: string]: GraphQLError } | undefined;
+  };
 }
 
 export const contextRef: { current: Context | null } = { current: null };
 
+// Checks whether the current data field is a cache miss because of a GraphQLError
+export const getFieldError = (ctx: Context): GraphQLError | undefined =>
+  ctx.__internal.path.length > 0 && ctx.__internal.errorMap
+    ? ctx.__internal.errorMap[ctx.__internal.path.join('.')]
+    : undefined;
+
 export const makeContext = (
   store: Store,
   variables: Variables,
   fragments: Fragments,
   typename: string,
   entityKey: string,
-  optimistic?: boolean
-): Context => ({
-  store,
-  variables,
-  fragments,
-  parent: { __typename: typename },
-  parentTypeName: typename,
-  parentKey: entityKey,
-  parentFieldKey: '',
-  fieldName: '',
-  partial: false,
-  optimistic: !!optimistic,
-});
+  optimistic?: boolean,
+  error?: CombinedError | undefined
+): Context => {
+  const ctx: Context = {
+    store,
+    variables,
+    fragments,
+    parent: { __typename: typename },
+    parentTypeName: typename,
+    parentKey: entityKey,
+    parentFieldKey: '',
+    fieldName: '',
+    error: undefined,
+    partial: false,
+    optimistic: !!optimistic,
+    __internal: {
+      path: [],
+      errorMap: undefined,
+    },
+  };
+
+  if (error && error.graphQLErrors) {
+    for (let i = 0; i < error.graphQLErrors.length; i++) {
+      const graphQLError = error.graphQLErrors[i];
+      if (graphQLError.path && graphQLError.path.length) {
+        if (!ctx.__internal.errorMap)
+          ctx.__internal.errorMap = Object.create(null);
+        ctx.__internal.errorMap![graphQLError.path.join('.')] = graphQLError;
+      }
+    }
+  }
+
+  return ctx;
+};
 
 export const updateContext = (
   ctx: Context,
@@ -64,6 +102,7 @@ export const updateContext = (
   ctx.parentKey = entityKey;
   ctx.parentFieldKey = fieldKey;
   ctx.fieldName = fieldName;
+  ctx.error = getFieldError(ctx);
 };
 
 const isFragmentHeuristicallyMatching = (
diff --git a/exchanges/graphcache/src/operations/write.ts b/exchanges/graphcache/src/operations/write.ts
index d45d2b17..fbc44105 100644
--- a/exchanges/graphcache/src/operations/write.ts
+++ b/exchanges/graphcache/src/operations/write.ts
@@ -1,4 +1,5 @@
 import { FieldNode, DocumentNode, FragmentDefinitionNode } from 'graphql';
+import { CombinedError } from '@urql/core';
 
 import {
   getFragments,
@@ -35,12 +36,14 @@ import {
 } from '../store';
 
 import * as InMemoryData from '../store/data';
+
 import {
   Context,
   makeSelectionIterator,
   ensureData,
   makeContext,
   updateContext,
+  getFieldError,
 } from './shared';
 
 export interface WriteResult {
@@ -53,85 +56,66 @@ export const write = (
   store: Store,
   request: OperationRequest,
   data: Data,
+  error?: CombinedError | undefined,
   key?: number
 ): WriteResult => {
   initDataState('write', store.data, key || null);
-  const result = startWrite(store, request, data);
+  const result = startWrite(store, request, data, error);
   clearDataState();
   return result;
 };
 
-export const startWrite = (
+export const writeOptimistic = (
   store: Store,
   request: OperationRequest,
-  data: Data
-) => {
-  const operation = getMainOperation(request.query);
-  const result: WriteResult = { data, dependencies: getCurrentDependencies() };
-  const operationName = store.rootFields[operation.operation];
-
-  const ctx = makeContext(
-    store,
-    normalizeVariables(operation, request.variables),
-    getFragments(request.query),
-    operationName,
-    operationName
-  );
-
-  if (process.env.NODE_ENV !== 'production') {
-    pushDebugNode(operationName, operation);
-  }
-
-  writeSelection(ctx, operationName, getSelectionSet(operation), data);
-
+  key: number
+): WriteResult => {
   if (process.env.NODE_ENV !== 'production') {
-    popDebugNode();
+    invariant(
+      getMainOperation(request.query).operation === 'mutation',
+      'writeOptimistic(...) was called with an operation that is not a mutation.\n' +
+        'This case is unsupported and should never occur.',
+      10
+    );
   }
 
+  initDataState('write', store.data, key, true);
+  const result = startWrite(store, request, {} as Data, undefined, true);
+  clearDataState();
   return result;
 };
 
-export const writeOptimistic = (
+export const startWrite = (
   store: Store,
   request: OperationRequest,
-  key: number
-): WriteResult => {
-  initDataState('write', store.data, key, true);
-
+  data: Data,
+  error?: CombinedError | undefined,
+  isOptimistic?: boolean
+) => {
   const operation = getMainOperation(request.query);
-  const result: WriteResult = {
-    data: {} as Data,
-    dependencies: getCurrentDependencies(),
-  };
+  const result: WriteResult = { data, dependencies: getCurrentDependencies() };
   const operationName = store.rootFields[operation.operation];
 
-  invariant(
-    operationName === store.rootFields['mutation'],
-    'writeOptimistic(...) was called with an operation that is not a mutation.\n' +
-      'This case is unsupported and should never occur.',
-    10
-  );
-
-  if (process.env.NODE_ENV !== 'production') {
-    pushDebugNode(operationName, operation);
-  }
-
   const ctx = makeContext(
     store,
     normalizeVariables(operation, request.variables),
     getFragments(request.query),
     operationName,
     operationName,
-    true
+    !!isOptimistic,
+    error
   );
 
-  writeSelection(ctx, operationName, getSelectionSet(operation), result.data!);
+  if (process.env.NODE_ENV !== 'production') {
+    pushDebugNode(operationName, operation);
+  }
+
+  writeSelection(ctx, operationName, getSelectionSet(operation), data);
 
   if (process.env.NODE_ENV !== 'production') {
     popDebugNode();
   }
 
-  clearDataState();
   return result;
 };
 
@@ -174,7 +158,8 @@ export const writeFragment = (
     variables || {},
     fragments,
     typename,
-    entityKey
+    entityKey,
+    undefined
   );
 
   writeSelection(ctx, entityKey, getSelectionSet(fragment), dataToWrite);
@@ -246,9 +231,14 @@ const writeSelection = (
       }
     }
 
-    if (fieldName === '__typename') {
-      continue;
-    } else if (ctx.optimistic && isRoot) {
+    // We simply skip all typenames fields and assume they've already been written above
+    if (fieldName === '__typename') continue;
+
+    // Add the current alias to the walked path before processing the field's value
+    ctx.__internal.path.push(fieldAlias);
+
+    // Execute optimistic mutation functions on root fields
+    if (ctx.optimistic && isRoot) {
       const resolver = ctx.store.optimisticMutations[fieldName];
 
       if (!resolver) continue;
@@ -278,29 +268,34 @@ const writeSelection = (
       InMemoryData.writeRecord(
         entityKey || typename,
         fieldKey,
-        fieldValue as EntityField
+        (fieldValue !== null || !getFieldError(ctx)
+          ? fieldValue
+          : undefined) as EntityField
       );
     }
 
     if (isRoot) {
-      // We have to update the context to reflect up-to-date ResolveInfo
-      updateContext(
-        ctx,
-        data,
-        typename,
-        typename,
-        joinKeys(typename, fieldKey),
-        fieldName
-      );
-
       // We run side-effect updates after the default, normalized updates
       // so that the data is already available in-store if necessary
       const updater = ctx.store.updates[typename][fieldName];
       if (updater) {
+        // We have to update the context to reflect up-to-date ResolveInfo
+        updateContext(
+          ctx,
+          data,
+          typename,
+          typename,
+          joinKeys(typename, fieldKey),
+          fieldName
+        );
+
         data[fieldName] = fieldValue;
         updater(data, fieldArgs || {}, ctx.store, ctx);
       }
     }
+
+    // After processing the field, remove the current alias from the path again
+    ctx.__internal.path.pop();
   }
 };
 
@@ -312,24 +307,27 @@ const writeField = (
   select: SelectionSet,
   data: null | Data | NullArray<Data>,
   parentFieldKey?: string
-): Link => {
+): Link | undefined => {
   if (Array.isArray(data)) {
     const newData = new Array(data.length);
     for (let i = 0, l = data.length; i < l; i++) {
-      const item = data[i];
+      // Add the current index to the walked path before processing the link
+      ctx.__internal.path.push(i);
       // Append the current index to the parentFieldKey fallback
       const indexKey = parentFieldKey
         ? joinKeys(parentFieldKey, `${i}`)
         : undefined;
       // Recursively write array data
-      const links = writeField(ctx, select, item, indexKey);
+      const links = writeField(ctx, select, data[i], indexKey);
       // Link cannot be expressed as a recursive type
       newData[i] = links as string | null;
+      // After processing the field, remove the current index from the path
+      ctx.__internal.path.pop();
     }
 
     return newData;
   } else if (data === null) {
-    return null;
+    return getFieldError(ctx) ? undefined : null;
   }
 
   const entityKey = ctx.store.keyOfEntity(data);
diff --git a/exchanges/graphcache/src/types.ts b/exchanges/graphcache/src/types.ts
index 1dec403b..41561628 100644
--- a/exchanges/graphcache/src/types.ts
+++ b/exchanges/graphcache/src/types.ts
@@ -1,5 +1,5 @@
 import { TypedDocumentNode } from '@urql/core';
-import { DocumentNode, FragmentDefinitionNode } from 'graphql';
+import { GraphQLError, DocumentNode, FragmentDefinitionNode } from 'graphql';
 
 // Helper types
 export type NullArray<T> = Array<null | T>;
@@ -65,8 +65,10 @@ export interface ResolveInfo {
   fieldName: string;
   fragments: Fragments;
   variables: Variables;
+  error: GraphQLError | undefined;
   partial?: boolean;
   optimistic?: boolean;
+  __internal?: unknown;
 }
 
 export interface QueryInput<T = Data, V = Variables> {
diff --git a/packages/site/static.config.js b/packages/site/static.config.js
index 6dbd9cba..0de5caed 100644
--- a/packages/site/static.config.js
+++ b/packages/site/static.config.js
@@ -50,6 +50,26 @@ export default {
       path: '/',
       template: require.resolve('./src/screens/home'),
     },
+    {
+      path: '/docs/concepts/core-package',
+      redirect: 'docs/basics/core',
+    },
+    {
+      path: '/docs/basics/getting-started',
+      redirect: 'docs/basics',
+    },
+    {
+      path: '/docs/basics/mutations',
+      redirect: 'docs/basics',
+    },
+    {
+      path: '/docs/basics/queries',
+      redirect: 'docs/basics',
+    },
+    {
+      path: '/docs/basics/document-caching',
+      redirect: 'docs/concepts/document-caching',
+    },
     {
       path: '404',
       template: require.resolve('./src/screens/404'),


Test patch:
diff --git a/exchanges/graphcache/src/cacheExchange.test.ts b/exchanges/graphcache/src/cacheExchange.test.ts
index b36bc959..fed11aed 100644
--- a/exchanges/graphcache/src/cacheExchange.test.ts
+++ b/exchanges/graphcache/src/cacheExchange.test.ts
@@ -4,6 +4,7 @@ import {
   ExchangeIO,
   Operation,
   OperationResult,
+  CombinedError,
 } from '@urql/core';
 
 import {
@@ -610,6 +611,69 @@ describe('data dependencies', () => {
     jest.runAllTimers();
     expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2);
   });
+
+  it('marks errored null fields as uncached but delivers them as expected', () => {
+    const client = createClient({ url: 'http://0.0.0.0' });
+    const { source: ops$, next } = makeSubject<Operation>();
+
+    const query = gql`
+      {
+        field
+        author {
+          id
+        }
+      }
+    `;
+
+    const operation = client.createRequestOperation('query', {
+      key: 1,
+      query,
+    });
+
+    const queryResult: OperationResult = {
+      operation,
+      data: {
+        __typename: 'Query',
+        field: 'test',
+        author: null,
+      },
+      error: new CombinedError({
+        graphQLErrors: [
+          {
+            message: 'Test',
+            path: ['author'],
+          },
+        ],
+      }),
+    };
+
+    const reexecuteOperation = jest
+      .spyOn(client, 'reexecuteOperation')
+      .mockImplementation(next);
+
+    const response = jest.fn(
+      (forwardOp: Operation): OperationResult => {
+        if (forwardOp.key === 1) return queryResult;
+        return undefined as any;
+      }
+    );
+
+    const result = jest.fn();
+    const forward: ExchangeIO = ops$ => pipe(ops$, map(response));
+
+    pipe(
+      cacheExchange({})({ forward, client, dispatchDebug })(ops$),
+      tap(result),
+      publish
+    );
+
+    next(operation);
+
+    expect(response).toHaveBeenCalledTimes(1);
+    expect(result).toHaveBeenCalledTimes(1);
+    expect(reexecuteOperation).toHaveBeenCalledTimes(0);
+    expect(result.mock.calls[0][0]).toHaveProperty('data.author', null);
+  });
 });
 
 describe('optimistic updates', () => {
diff --git a/exchanges/graphcache/src/operations/write.test.ts b/exchanges/graphcache/src/operations/write.test.ts
index 901431d0..773838e8 100644
--- a/exchanges/graphcache/src/operations/write.test.ts
+++ b/exchanges/graphcache/src/operations/write.test.ts
@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-var-requires */
 
-import { gql } from '@urql/core';
+import { gql, CombinedError } from '@urql/core';
 import { minifyIntrospectionQuery } from '@urql/introspection';
 
 import { write } from './write';
@@ -174,4 +174,77 @@ describe('Query', () => {
     // The field must still be `'test'`
     expect(InMemoryData.readRecord('Query', 'field')).toBe('test');
   });
+
+  it('should write errored records as undefined rather than null', () => {
+    const query = gql`
+      {
+        missingField
+        setField
+      }
+    `;
+
+    write(
+      store,
+      { query },
+      { missingField: null, setField: 'test' } as any,
+      new CombinedError({
+        graphQLErrors: [
+          {
+            message: 'Test',
+            path: ['missingField'],
+          },
+        ],
+      })
+    );
+
+    InMemoryData.initDataState('read', store.data, null);
+
+    // The setField must still be `'test'`
+    expect(InMemoryData.readRecord('Query', 'setField')).toBe('test');
+    // The missingField must still be `undefined`
+    expect(InMemoryData.readRecord('Query', 'missingField')).toBe(undefined);
+  });
+
+  it('should write errored links as undefined rather than null', () => {
+    const query = gql`
+      {
+        missingTodoItem: todos {
+          id
+          text
+        }
+        missingTodo: todo {
+          id
+          text
+        }
+      }
+    `;
+
+    write(
+      store,
+      { query },
+      {
+        missingTodoItem: [null, { __typename: 'Todo', id: 1, text: 'Learn' }],
+        missingTodo: null,
+      } as any,
+      new CombinedError({
+        graphQLErrors: [
+          {
+            message: 'Test',
+            path: ['missingTodoItem', 0],
+          },
+          {
+            message: 'Test',
+            path: ['missingTodo'],
+          },
+        ],
+      })
+    );
+
+    InMemoryData.initDataState('read', store.data, null);
+    expect(InMemoryData.readLink('Query', 'todos')).toEqual([
+      undefined,
+      'Todo:1',
+    ]);
+    expect(InMemoryData.readLink('Query', 'todo')).toEqual(undefined);
+  });
 });