Repository: FormidableLabs/urql
Pull request #329 (2019-07-02 20:46:26)

Replace useDevtoolsContext Error.stack with Fiber approach
(I added some tests for `useDevtoolsContext` first, before I started, just to check that it's still doing the right thing)

This replaces the `Error` stack to determine the current component name with a React Fiber approach, by looking at `ReactCurrentOwner`. This is generally safe and the data structure hasn't changed much since `react-reconciler` was implemented. There's also some guards in the implementation though just in case. But this is just for development after all.

The motivation behind this was that the stack approach can lead to incorrect names at times depending on the browser, minification in the app bundle, testing approach, and other factors, where the stack is unreliable.

So this looks at `ReactCurrentOwner`, which is a ref, checks whether we have a fiber (which we should) and whether it's a component (which it must be since we're checking from a hook). Then it gets the component and will use `displayName` (hooray, display names!) or `name` to determine the current component's name.

If the name is found to be an internal one (`Query`, `Mutation`, `Subscription`) then it'll walk up by one fiber and use the parent instead to get a more useful name. This is currently done using a heuristic as it should be sufficient for a development mode implementation.

I also added some support for class components, since walking one component up via the error stack is pretty tough (if not impossible?)

@andyrichardson: Hope you like it :tada: tl;dr I think it's potentially more stable and allows us to get the `displayName` and find the parent component for our own components :+1:

parkerziegler: Wowza, this is pretty dang 🆒! I like the implementation a lot @kitten 🎉   
andyrichardson: @kitten what's the inventive behind the `void` additions?
kitten: @andyrichardson it’s definitely still a little dodgy but stable. I already had this in the back of my mind for the chance that the error stacktraces become unreliable. It is the internal Fiber architecture, but since this is one of the most important React internals it won’t just disappear, and if it does there are some guards.

The void additions are because TS didn’t expect undefined arguments anymore for the options or operation spreads once the destructure was gone for some reason.

Regarding removing the array destructure itself, it’s an internal hook and there’s no convention around making hooks return arrays/tuples, that’s just a convention for our public hooks.

Since the typings document this return value internally and externally this hooks isn’t in use, I think it’s unnecessary to keep it :)
andyrichardson: > The void additions are because TS didn’t expect undefined arguments anymore for the options or operation spreads once the destructure was gone for some reason.

I'm not sure adding void is a good way to solve this problem given we're only expecting our argument properties to be of type `T | undefined`. Can you drop the error message you're getting here?



Patch:
diff --git a/docs/api.md b/docs/api.md
index b00d8012..fbbe6e72 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -73,7 +73,7 @@ interface UseSubscriptionArgs {
 And the handler has the signature:
 
 ```js
-type SubscriptionHandler<T, R> = (prev: R | void, data: T) => R;
+type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
 ```
 
 Meaning that the subscription handler receives the previous data or undefined
@@ -139,12 +139,12 @@ interface UseSubscriptionState<T> {
 
 #### Props
 
-| Prop      | Type                                      | Description                                                                                           |
-| --------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- |
-| query     | `string`                                  | The GraphQL subscription's query                                                                      |
-| variables | `object`                                  | The GraphQL subscriptions' variables                                                                  |
-| handler   | `void \| (prev: R \| void, data: T) => R` | The handler that should combine/update the subscription's data with incoming data                     |
-| children  | `RenderProps => ReactNode`                | A function that follows the typical render props pattern. The shape of the render props is as follows |
+| Prop      | Type                                                | Description                                                                                           |
+| --------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| query     | `string`                                            | The GraphQL subscription's query                                                                      |
+| variables | `object`                                            | The GraphQL subscriptions' variables                                                                  |
+| handler   | `undefined \| (prev: R \| undefined, data: T) => R` | The handler that should combine/update the subscription's data with incoming data                     |
+| children  | `RenderProps => ReactNode`                          | A function that follows the typical render props pattern. The shape of the render props is as follows |
 
 #### Render Props
 
diff --git a/docs/basics.md b/docs/basics.md
index ac5bf6c4..2fbf8c85 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -75,7 +75,7 @@ cache"_ but more of a _"document cache"_.
 
 ### The document cache
 
-When an **operation** is sent it is identifier via its `key` which is a hash
+When an **operation** is sent it is identified by its `key` which is a hash
 of the `query` and `variables`. A document cache makes the assumption
 that there's **no overlap** between any two given queries.
 
@@ -113,7 +113,7 @@ respond with the types that are invalidated.
 Given an `addTodo` mutation for example, you will need to send
 back at least one `TodoItem` for the invalidation to happen.
 
-a **document cache** also doesn't normalise at all, which means
+A **document cache** also doesn't normalise at all, which means
 that after fetching a list of items, fetching a single item
 will never be fulfilled by this cache.
 
@@ -129,7 +129,7 @@ cached result is available it will let the operation through, so
 that the `fetchExchange` can send a request to the API.
 
 When `'cache-only'` is passed, the cache will always return the
-cached result or default to `{ data: undefined, error: undefined },
+cached result or default to `{ data: undefined, error: undefined }`,
 i.e. an empty result, when nothing is cached for a given operation.
 
 For `'network-only'` the opposite of `'cache-only'` is done.
@@ -139,7 +139,7 @@ so that the `fetchExchange` can respond with up-to-date data.
 The result will still be cached however.
 
 The last one `'cache-and-network'` is rather special
-in that it first does what `'cache-first'` does, it will
+in that it first does what `'cache-first'` does: it will
 return some cached results. After returning a cached result however,
 it will forward the operation anyway. This way a temporary cached
 result may be displayed that is then updated with fresh data
diff --git a/docs/getting-started.md b/docs/getting-started.md
index f4792fc3..75114ab1 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -236,14 +236,13 @@ const addTodo = `
 
 class TodoForm extends Component {
   state = {
-    error: null
+    error: null,
   };
 
   add = () => {
-    this.props.addTodo({ text: 'something!' })
-      .catch(error => {
-        this.setState({ error });
-      });
+    this.props.addTodo({ text: 'something!' }).catch(error => {
+      this.setState({ error });
+    });
   };
 
   render() {
@@ -251,13 +250,13 @@ class TodoForm extends Component {
       return 'Oh no!';
     }
 
-    return <button onClick={this.add}>Add something!</button>
+    return <button onClick={this.add}>Add something!</button>;
   }
 }
 
 const WithMutation = () => (
   <Mutation query={addTodo}>
-    {({ executeMutation }) => <TodoForm addTodo={executeMutation} />
+    {({ executeMutation }) => <TodoForm addTodo={executeMutation} />}
   </Mutation>
 );
 ```
@@ -291,7 +290,7 @@ class TodoForm extends Component {
 
 const WithMutation = () => (
   <Mutation query={addTodo}>
-    {({ error, executeMutation }) => <TodoForm error={error} addTodo={executeMutation} />
+    {({ error, executeMutation }) => <TodoForm error={error} addTodo={executeMutation} />}
   </Mutation>
 );
 ```
diff --git a/examples/1-getting-started/src/app/Home.tsx b/examples/1-getting-started/src/app/Home.tsx
index 73e76995..d37c959b 100644
--- a/examples/1-getting-started/src/app/Home.tsx
+++ b/examples/1-getting-started/src/app/Home.tsx
@@ -46,6 +46,8 @@ export const Home: FC = () => {
   );
 };
 
+Home.displayName = 'Home';
+
 const TodoQuery = gql`
   query {
     todos {
diff --git a/examples/1-getting-started/src/app/components/Error.tsx b/examples/1-getting-started/src/app/components/Error.tsx
index cfcf603f..c54fc3eb 100644
--- a/examples/1-getting-started/src/app/components/Error.tsx
+++ b/examples/1-getting-started/src/app/components/Error.tsx
@@ -7,3 +7,5 @@ export const Error: FC = props => (
     <p>Message: {props.children}</p>
   </>
 );
+
+Error.displayName = 'Error';
diff --git a/examples/1-getting-started/src/app/components/Loading.tsx b/examples/1-getting-started/src/app/components/Loading.tsx
index adcd8e83..41a900b1 100644
--- a/examples/1-getting-started/src/app/components/Loading.tsx
+++ b/examples/1-getting-started/src/app/components/Loading.tsx
@@ -1,3 +1,5 @@
 import React from 'react';
 
 export const Loading = () => <p>Loading...</p>;
+
+Loading.displayName = 'Loading';
diff --git a/examples/1-getting-started/src/app/components/Todo.tsx b/examples/1-getting-started/src/app/components/Todo.tsx
index 85428c8a..10817fed 100644
--- a/examples/1-getting-started/src/app/components/Todo.tsx
+++ b/examples/1-getting-started/src/app/components/Todo.tsx
@@ -23,9 +23,9 @@ export const Todo: FC<Props> = props => {
 Todo.displayName = 'Todo';
 
 const RemoveTodo = `
-mutation($id: ID!) {
-  toggleTodo(id: $id) {
-    id
+  mutation($id: ID!) {
+    toggleTodo(id: $id) {
+      id
+    }
   }
-}
 `;
diff --git a/examples/1-getting-started/src/app/index.tsx b/examples/1-getting-started/src/app/index.tsx
index 235350ca..50d36dde 100644
--- a/examples/1-getting-started/src/app/index.tsx
+++ b/examples/1-getting-started/src/app/index.tsx
@@ -17,4 +17,6 @@ export const App: FC = () => (
   </Provider>
 );
 
+App.displayName = 'App';
+
 ReactDOM.render(<App />, document.getElementById('root'));
diff --git a/examples/1-getting-started/yarn.lock b/examples/1-getting-started/yarn.lock
index 91dd277f..17337c50 100644
--- a/examples/1-getting-started/yarn.lock
+++ b/examples/1-getting-started/yarn.lock
@@ -14,22 +14,6 @@
   resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3"
   integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ==
 
-"@babel/runtime@^7.4.5":
-  version "7.4.5"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
-  integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
-  dependencies:
-    regenerator-runtime "^0.13.2"
-
-"@jest/types@^24.8.0":
-  version "24.8.0"
-  resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad"
-  integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==
-  dependencies:
-    "@types/istanbul-lib-coverage" "^2.0.0"
-    "@types/istanbul-reports" "^1.1.1"
-    "@types/yargs" "^12.0.9"
-
 "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -83,30 +67,6 @@
   resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
   integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
 
-"@sheerun/mutationobserver-shim@^0.3.2":
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
-  integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
-
-"@testing-library/dom@^5.0.0":
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-5.2.0.tgz#450bd09f93ddd8865eec98fc5e81fceb2bccdf9c"
-  integrity sha512-nFaZes/bzDfMqwZpQXdiPyj3WXU16FYf5k5NCFu/qJM4JdRJLHEtSRYtrETmk7nCf+qLVoHCqRduGi/4KE83Gw==
-  dependencies:
-    "@babel/runtime" "^7.4.5"
-    "@sheerun/mutationobserver-shim" "^0.3.2"
-    aria-query "3.0.0"
-    pretty-format "^24.8.0"
-    wait-for-expect "^1.2.0"
-
-"@testing-library/react@^8.0.1":
-  version "8.0.1"
-  resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-8.0.1.tgz#91c254adf855b13de50020613cb5d3915f9f7875"
-  integrity sha512-N/1pJfhEnNYkGyxuw4xbp03evaS0z/CT8o0QgTfJqGlukAcU15xf9uU1w03NHKZJcU69nOCBAoAkXHtHzYwMbg==
-  dependencies:
-    "@babel/runtime" "^7.4.5"
-    "@testing-library/dom" "^5.0.0"
-
 "@types/accepts@^1.3.5":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
@@ -158,31 +118,6 @@
     "@types/express-serve-static-core" "*"
     "@types/serve-static" "*"
 
-"@types/fast-json-stable-stringify@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#40363bb847cb86b2c2e1599f1398d11e8329c921"
-  integrity sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ==
-
-"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
-  integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==
-
-"@types/istanbul-lib-report@*":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c"
-  integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==
-  dependencies:
-    "@types/istanbul-lib-coverage" "*"
-
-"@types/istanbul-reports@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
-  integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==
-  dependencies:
-    "@types/istanbul-lib-coverage" "*"
-    "@types/istanbul-lib-report" "*"
-
 "@types/long@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
@@ -224,11 +159,6 @@
     "@types/events" "*"
     "@types/node" "*"
 
-"@types/yargs@^12.0.9":
-  version "12.0.12"
-  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
-  integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
-
 "@webassemblyjs/ast@1.7.11":
   version "1.7.11"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
@@ -445,12 +375,7 @@ ansi-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
   integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
 
-ansi-regex@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
-  integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
-
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
   integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
@@ -620,14 +545,6 @@ are-we-there-yet@~1.1.2:
     delegates "^1.0.0"
     readable-stream "^2.0.6"
 
-aria-query@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
-  integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=
-  dependencies:
-    ast-types-flow "0.0.7"
-    commander "^2.11.0"
-
 arr-diff@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -706,11 +623,6 @@ assign-symbols@^1.0.0:
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
 
-ast-types-flow@0.0.7:
-  version "0.0.7"
-  resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
-  integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
-
 async-each@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -1106,11 +1018,6 @@ color-name@1.1.3:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
   integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
 
-commander@^2.11.0:
-  version "2.20.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
-  integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
-
 commander@~2.17.1:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
@@ -3590,16 +3497,6 @@ postcss@^7.0.5, postcss@^7.0.6:
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
-pretty-format@^24.8.0:
-  version "24.8.0"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"
-  integrity sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==
-  dependencies:
-    "@jest/types" "^24.8.0"
-    ansi-regex "^4.0.0"
-    ansi-styles "^3.2.0"
-    react-is "^16.8.4"
-
 process-nextick-args@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
@@ -3783,11 +3680,6 @@ react-is@^16.8.1:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb"
   integrity sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA==
 
-react-is@^16.8.4:
-  version "16.8.6"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
-  integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
-
 react@^16.8.1:
   version "16.8.1"
   resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a"
@@ -3843,11 +3735,6 @@ regenerate@^1.2.1:
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
   integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
 
-regenerator-runtime@^0.13.2:
-  version "0.13.2"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
-  integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
-
 regex-not@^1.0.0, regex-not@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -4731,11 +4618,6 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
-wait-for-expect@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.2.0.tgz#fdab6a26e87d2039101db88bff3d8158e5c3e13f"
-  integrity sha512-EJhKpA+5UHixduMBEGhTFuLuVgQBKWxkFbefOdj2bbk2/OpA5Opsc4aUTGmF+qJ+v3kTGxDRNYwKaT4j6g5n8Q==
-
 watchpack@^1.5.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
diff --git a/examples/2-using-subscriptions/README.md b/examples/2-using-subscriptions/README.md
index 99eca6e6..bcb4f6b8 100644
--- a/examples/2-using-subscriptions/README.md
+++ b/examples/2-using-subscriptions/README.md
@@ -1,21 +1,35 @@
 ## About
 
-This is a basic example of subscriptions being used with URQL.
+This is a basic example of subscriptions being used with `urql`. It demonstrates some of the core principles around wiring up subscriptions, including:
+
+- Setting up the `subscriptionExchange` with a `subscriptionClient`.
+- Using the `useSubscription` hook.
+- Using a `handler` to accumulate the results of subscriptions.
 
 ## Usage
 
-#### 1. Install dependencies
+#### 1. Install dependencies in repo root.
+
+To get started with the example, make sure to install `urql`'s dependencies in the root directory of your clone. We `link` to the `urql` source from the `examples` directory, so its dependencies need to be installed.
+
+```bash
+# In root directory
+yarn
+```
+
+#### 2. Navigate to this directory and install dependencies.
 
 ```bash
+cd examples/2-using-subscriptions
 yarn
 ```
 
-#### 2. Start server
+#### 3. Start the example app and server (this is done in a single command).
 
 ```bash
 yarn start
 ```
 
-#### 3. Open browser
+#### 4. Open your browser
 
 Navigate to example at [http://localhost:3000/](http://localhost:3000/).
diff --git a/examples/2-using-subscriptions/src/app/Messages.tsx b/examples/2-using-subscriptions/src/app/Messages.tsx
index 7719db61..3c0c4c02 100644
--- a/examples/2-using-subscriptions/src/app/Messages.tsx
+++ b/examples/2-using-subscriptions/src/app/Messages.tsx
@@ -31,6 +31,8 @@ export const Messages: FC = () => {
   );
 };
 
+Messages.displayName = 'Messages';
+
 const NewMessageSubQuery = gql`
   subscription messageSub {
     newMessages {
diff --git a/examples/2-using-subscriptions/src/app/components/Error.tsx b/examples/2-using-subscriptions/src/app/components/Error.tsx
index cfcf603f..c54fc3eb 100644
--- a/examples/2-using-subscriptions/src/app/components/Error.tsx
+++ b/examples/2-using-subscriptions/src/app/components/Error.tsx
@@ -7,3 +7,5 @@ export const Error: FC = props => (
     <p>Message: {props.children}</p>
   </>
 );
+
+Error.displayName = 'Error';
diff --git a/examples/2-using-subscriptions/src/app/components/Message.tsx b/examples/2-using-subscriptions/src/app/components/Message.tsx
index 2881b6b0..05e2ba88 100644
--- a/examples/2-using-subscriptions/src/app/components/Message.tsx
+++ b/examples/2-using-subscriptions/src/app/components/Message.tsx
@@ -16,3 +16,5 @@ export const Message: FC<MessageEntry> = props => (
     <li>{props.message}</li>
   </div>
 );
+
+Message.displayName = 'Message';
diff --git a/examples/2-using-subscriptions/src/app/index.tsx b/examples/2-using-subscriptions/src/app/index.tsx
index 8454c45f..c6b6f893 100644
--- a/examples/2-using-subscriptions/src/app/index.tsx
+++ b/examples/2-using-subscriptions/src/app/index.tsx
@@ -38,4 +38,6 @@ export const App: FC = () => (
   </Provider>
 );
 
+App.displayName = 'App';
+
 ReactDOM.render(<App />, document.getElementById('root'));
diff --git a/examples/3-ssr-with-nextjs/components/App.js b/examples/3-ssr-with-nextjs/components/App.js
index 49b8b547..fc3e4ed5 100644
--- a/examples/3-ssr-with-nextjs/components/App.js
+++ b/examples/3-ssr-with-nextjs/components/App.js
@@ -1,4 +1,4 @@
-export default ({ children }) => (
+const App = ({ children }) => (
   <main>
     {children}
     <style jsx global>{`
@@ -40,3 +40,5 @@ export default ({ children }) => (
     `}</style>
   </main>
 );
+
+export default App;
diff --git a/examples/3-ssr-with-nextjs/components/ErrorMessage.js b/examples/3-ssr-with-nextjs/components/ErrorMessage.js
index 69f12547..77f1c67c 100644
--- a/examples/3-ssr-with-nextjs/components/ErrorMessage.js
+++ b/examples/3-ssr-with-nextjs/components/ErrorMessage.js
@@ -1,4 +1,4 @@
-export default ({ message }) => (
+const ErrorMessage = ({ message }) => (
   <aside>
     {message}
     <style jsx>{`
@@ -11,3 +11,5 @@ export default ({ message }) => (
     `}</style>
   </aside>
 );
+
+export default ErrorMessage;
diff --git a/examples/3-ssr-with-nextjs/pages/index.js b/examples/3-ssr-with-nextjs/pages/index.js
index 2283aa61..99761140 100644
--- a/examples/3-ssr-with-nextjs/pages/index.js
+++ b/examples/3-ssr-with-nextjs/pages/index.js
@@ -2,9 +2,11 @@ import App from '../components/App';
 import Submit from '../components/Submit';
 import PostList from '../components/PostList';
 
-export default () => (
+const Root = () => (
   <App>
     <Submit />
     <PostList />
   </App>
 );
+
+export default Root;
diff --git a/src/components/Mutation.ts b/src/components/Mutation.ts
index 537c010f..a8a4a6c5 100644
--- a/src/components/Mutation.ts
+++ b/src/components/Mutation.ts
@@ -1,11 +1,11 @@
-import { ReactNode } from 'react';
+import { ReactElement } from 'react';
 import { DocumentNode } from 'graphql';
 import { OperationResult } from '../types';
 import { useMutation, UseMutationState } from '../hooks';
 
 export interface MutationProps<T, V> {
   query: DocumentNode | string;
-  children: (arg: MutationState<T, V>) => ReactNode;
+  children: (arg: MutationState<T, V>) => ReactElement<any>;
 }
 
 export interface MutationState<T, V> extends UseMutationState<T> {
@@ -15,7 +15,7 @@ export interface MutationState<T, V> extends UseMutationState<T> {
 export function Mutation<T = any, V = any>({
   children,
   query,
-}: MutationProps<T, V>): ReactNode {
+}: MutationProps<T, V>): ReactElement<any> {
   const [state, executeMutation] = useMutation<T, V>(query);
   return children({ ...state, executeMutation });
 }
diff --git a/src/components/Query.ts b/src/components/Query.ts
index 206dd32d..6f0f4c8f 100644
--- a/src/components/Query.ts
+++ b/src/components/Query.ts
@@ -1,9 +1,9 @@
-import { ReactNode } from 'react';
+import { ReactElement } from 'react';
 import { OperationContext } from '../types';
 import { useQuery, UseQueryArgs, UseQueryState } from '../hooks';
 
 export interface QueryProps<T, V> extends UseQueryArgs<V> {
-  children: (arg: QueryState<T>) => ReactNode;
+  children: (arg: QueryState<T>) => ReactElement<any>;
 }
 
 export interface QueryState<T> extends UseQueryState<T> {
@@ -13,7 +13,7 @@ export interface QueryState<T> extends UseQueryState<T> {
 export function Query<T = any, V = any>({
   children,
   ...args
-}: QueryProps<T, V>): ReactNode {
+}: QueryProps<T, V>): ReactElement<any> {
   const [state, executeQuery] = useQuery<T, V>(args);
   return children({ ...state, executeQuery });
 }
diff --git a/src/components/Subscription.ts b/src/components/Subscription.ts
index c6a38920..0cac3b02 100644
--- a/src/components/Subscription.ts
+++ b/src/components/Subscription.ts
@@ -1,4 +1,4 @@
-import { ReactNode } from 'react';
+import { ReactElement } from 'react';
 
 import {
   useSubscription,
@@ -9,14 +9,14 @@ import {
 
 export interface SubscriptionProps<T, R, V> extends UseSubscriptionArgs<V> {
   handler?: SubscriptionHandler<T, R>;
-  children: (arg: UseSubscriptionState<R>) => ReactNode;
+  children: (arg: UseSubscriptionState<R>) => ReactElement<any>;
 }
 
 export function Subscription<T = any, R = T, V = any>({
   children,
   handler,
   ...args
-}: SubscriptionProps<T, R, V>): ReactNode {
+}: SubscriptionProps<T, R, V>): ReactElement<any> {
   const [state] = useSubscription<T, R, V>(args, handler);
   return children(state);
 }
diff --git a/src/exchanges/fetch.ts b/src/exchanges/fetch.ts
index 9208e6eb..6b13058a 100755
--- a/src/exchanges/fetch.ts
+++ b/src/exchanges/fetch.ts
@@ -136,7 +136,7 @@ const executeFetch = (operation: Operation, opts: RequestInit) => {
 const checkStatus = (redirectMode: string = 'follow', response: Response) => {
   const statusRangeEnd = redirectMode === 'manual' ? 400 : 300;
 
-  if (response.status < 200 || response.status > statusRangeEnd) {
+  if (response.status < 200 || response.status >= statusRangeEnd) {
     throw new Error(response.statusText);
   }
 };
diff --git a/src/hooks/useDevtoolsContext.ts b/src/hooks/useDevtoolsContext.ts
index c8f122a6..dc257953 100644
--- a/src/hooks/useDevtoolsContext.ts
+++ b/src/hooks/useDevtoolsContext.ts
@@ -1,28 +1,50 @@
-import { useMemo, useRef } from 'react';
-
-/** Find the name of the calling component. */
-const getHookParent = () => {
-  /**
-   * Line values could be:
-   * - named export             " at exports.Home (Home.tsx?9b1b:17)"
-   * - default export / inline  " at Home (Home.tsx?9b1b:17)"
-   */
-  const stackLine = (new Error().stack as string).split('\n')[3];
-  const parsedLine = /.*at (exports\.)?(\w+)/.exec(stackLine);
-
-  return parsedLine === null || typeof parsedLine[2] !== 'string'
-    ? 'Unknown'
-    : parsedLine[2];
-};
+import * as React from 'react';
+import { OperationContext } from '../types';
+
+const {
+  ReactCurrentOwner: CurrentOwner,
+} = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
+
+// Is the Fiber a FunctionComponent, ClassComponent, or IndeterminateComponent
+const isComponentFiber = (fiber: void | { tag: number }) =>
+  fiber && (fiber.tag === 0 || fiber.tag === 1 || fiber.tag === 2);
+
+// Is the component one of ours (just a heuristic to avoid circular dependencies or flags)
+const isInternalComponent = (Component: { name: string }) =>
+  Component.name === 'Query' ||
+  Component.name === 'Mutation' ||
+  Component.name === 'Subscription';
+
+const useDevtoolsContextImpl = (): Partial<OperationContext> => {
+  return React.useMemo(() => {
+    let source = 'Component';
+
+    // Check whether the CurrentOwner is set
+    const owner = CurrentOwner.current;
+    if (owner !== null && isComponentFiber(owner)) {
+      let Component = owner.type;
+
+      // If this is one of our own components then check the parent
+      if (
+        isInternalComponent(Component) &&
+        isComponentFiber(owner._debugOwner)
+      ) {
+        Component = owner._debugOwner.type;
+      }
 
-const useDevtoolsContextHook = () => {
-  const source = useRef(getHookParent());
+      // Get the Component's name if it has one
+      if (typeof Component === 'function') {
+        source = Component.displayName || Component.name || source;
+      }
+    }
 
-  return useMemo(() => [{ meta: { source: source.current } }], []);
+    return { meta: { source } };
+  }, []);
 };
 
 /** Creates additional context values for serving metadata to devtools. */
-export const useDevtoolsContext =
-  process.env.NODE_ENV === 'production'
-    ? () => [undefined]
-    : useDevtoolsContextHook;
+export const useDevtoolsContext: () => Partial<OperationContext> | undefined =
+  // NOTE: We check for CurrentOwner in case it'll be unexpectedly changed in React's source
+  process.env.NODE_ENV !== 'production' && !!CurrentOwner
+    ? useDevtoolsContextImpl
+    : () => undefined;
diff --git a/src/hooks/useMutation.ts b/src/hooks/useMutation.ts
index 78fcd816..5cdc484e 100644
--- a/src/hooks/useMutation.ts
+++ b/src/hooks/useMutation.ts
@@ -21,7 +21,7 @@ export type UseMutationResponse<T, V> = [
 export const useMutation = <T = any, V = object>(
   query: DocumentNode | string
 ): UseMutationResponse<T, V> => {
-  const [devtoolsContext] = useDevtoolsContext();
+  const devtoolsContext = useDevtoolsContext();
   const client = useContext(Context);
   const [state, setState] = useImmediateState<UseMutationState<T>>({
     fetching: false,
diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts
index cb25c6ed..213920b0 100644
--- a/src/hooks/useQuery.ts
+++ b/src/hooks/useQuery.ts
@@ -30,7 +30,7 @@ export type UseQueryResponse<T> = [
 export const useQuery = <T = any, V = object>(
   args: UseQueryArgs<V>
 ): UseQueryResponse<T> => {
-  const [devtoolsContext] = useDevtoolsContext();
+  const devtoolsContext = useDevtoolsContext();
   const unsubscribe = useRef(noop);
   const client = useContext(Context);
 
diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts
index a5629217..aa89da90 100644
--- a/src/hooks/useRequest.ts
+++ b/src/hooks/useRequest.ts
@@ -8,7 +8,7 @@ export const useRequest = (
   query: string | DocumentNode,
   variables?: any
 ): GraphQLRequest => {
-  const prev = useRef<void | GraphQLRequest>(undefined);
+  const prev = useRef<undefined | GraphQLRequest>(undefined);
 
   return useMemo(() => {
     const request = createRequest(query, variables);
diff --git a/src/hooks/useSubscription.ts b/src/hooks/useSubscription.ts
index 42966481..87f528be 100644
--- a/src/hooks/useSubscription.ts
+++ b/src/hooks/useSubscription.ts
@@ -12,7 +12,7 @@ export interface UseSubscriptionArgs<V> {
   variables?: V;
 }
 
-export type SubscriptionHandler<T, R> = (prev: R | void, data: T) => R;
+export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
 
 export interface UseSubscriptionState<T> {
   fetching: boolean;
@@ -26,7 +26,7 @@ export const useSubscription = <T = any, R = T, V = object>(
   args: UseSubscriptionArgs<V>,
   handler?: SubscriptionHandler<T, R>
 ): UseSubscriptionResponse<R> => {
-  const [devtoolsContext] = useDevtoolsContext();
+  const devtoolsContext = useDevtoolsContext();
   const unsubscribe = useRef(noop);
   const client = useContext(Context);
 
diff --git a/src/types.ts b/src/types.ts
index 5eb12b8a..1c95278b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -29,17 +29,20 @@ export interface GraphQLRequest {
   variables?: object;
 }
 
+/** Metadata that is only available in development for devtools. */
+export interface OperationDebugMeta {
+  source?: string;
+  cacheOutcome?: CacheOutcome;
+  networkLatency?: number;
+}
+
 /** Additional metadata passed to [exchange]{@link Exchange} functions. */
 export interface OperationContext {
   [key: string]: any;
   fetchOptions?: RequestInit | (() => RequestInit);
   requestPolicy: RequestPolicy;
   url: string;
-  meta?: {
-    source?: string;
-    cacheOutcome?: CacheOutcome;
-    networkLatency?: number;
-  };
+  meta?: OperationDebugMeta;
 }
 
 /** A [query]{@link Query} or [mutation]{@link Mutation} with additional metadata for use during transmission. */
diff --git a/src/utils/typenames.ts b/src/utils/typenames.ts
index 384b594f..731df97d 100755
--- a/src/utils/typenames.ts
+++ b/src/utils/typenames.ts
@@ -16,7 +16,7 @@ const collectTypes = (obj: EntityLike | EntityLike[], types: string[] = []) => {
     obj.forEach(inner => collectTypes(inner, types));
   } else if (typeof obj === 'object' && obj !== null) {
     for (const key in obj) {
-      if (obj.hasOwnProperty(key)) {
+      if (Object.prototype.hasOwnProperty.call(obj, key)) {
         const val = obj[key];
         if (key === '__typename' && typeof val === 'string') {
           types.push(val);


Test patch:
diff --git a/src/exchanges/__snapshots__/fetch.test.ts.snap b/src/exchanges/__snapshots__/fetch.test.ts.snap
index 13a0174b..fa96ef54 100644
--- a/src/exchanges/__snapshots__/fetch.test.ts.snap
+++ b/src/exchanges/__snapshots__/fetch.test.ts.snap
@@ -131,6 +131,147 @@ Object {
 }
 `;
 
+exports[`on error returns error data with status 400 and manual redirect mode 1`] = `
+Object {
+  "data": undefined,
+  "error": [CombinedError: [Network] ],
+  "operation": Object {
+    "context": Object {
+      "fetchOptions": [MockFunction] {
+        "calls": Array [
+          Array [],
+        ],
+        "results": Array [
+          Object {
+            "type": "return",
+            "value": Object {
+              "redirect": "manual",
+            },
+          },
+        ],
+      },
+      "meta": Object {
+        "networkLatency": 100,
+      },
+      "requestPolicy": "cache-first",
+      "url": "http://localhost:3000/graphql",
+    },
+    "key": 2,
+    "operationName": "query",
+    "query": Object {
+      "definitions": Array [
+        Object {
+          "directives": Array [],
+          "kind": "OperationDefinition",
+          "name": Object {
+            "kind": "Name",
+            "value": "getUser",
+          },
+          "operation": "query",
+          "selectionSet": Object {
+            "kind": "SelectionSet",
+            "selections": Array [
+              Object {
+                "alias": undefined,
+                "arguments": Array [
+                  Object {
+                    "kind": "Argument",
+                    "name": Object {
+                      "kind": "Name",
+                      "value": "name",
+                    },
+                    "value": Object {
+                      "kind": "Variable",
+                      "name": Object {
+                        "kind": "Name",
+                        "value": "name",
+                      },
+                    },
+                  },
+                ],
+                "directives": Array [],
+                "kind": "Field",
+                "name": Object {
+                  "kind": "Name",
+                  "value": "user",
+                },
+                "selectionSet": Object {
+                  "kind": "SelectionSet",
+                  "selections": Array [
+                    Object {
+                      "alias": undefined,
+                      "arguments": Array [],
+                      "directives": Array [],
+                      "kind": "Field",
+                      "name": Object {
+                        "kind": "Name",
+                        "value": "id",
+                      },
+                      "selectionSet": undefined,
+                    },
+                    Object {
+                      "alias": undefined,
+                      "arguments": Array [],
+                      "directives": Array [],
+                      "kind": "Field",
+                      "name": Object {
+                        "kind": "Name",
+                        "value": "firstName",
+                      },
+                      "selectionSet": undefined,
+                    },
+                    Object {
+                      "alias": undefined,
+                      "arguments": Array [],
+                      "directives": Array [],
+                      "kind": "Field",
+                      "name": Object {
+                        "kind": "Name",
+                        "value": "lastName",
+                      },
+                      "selectionSet": undefined,
+                    },
+                  ],
+                },
+              },
+            ],
+          },
+          "variableDefinitions": Array [
+            Object {
+              "defaultValue": undefined,
+              "directives": Array [],
+              "kind": "VariableDefinition",
+              "type": Object {
+                "kind": "NamedType",
+                "name": Object {
+                  "kind": "Name",
+                  "value": "String",
+                },
+              },
+              "variable": Object {
+                "kind": "Variable",
+                "name": Object {
+                  "kind": "Name",
+                  "value": "name",
+                },
+              },
+            },
+          ],
+        },
+      ],
+      "kind": "Document",
+      "loc": Object {
+        "end": 124,
+        "start": 0,
+      },
+    },
+    "variables": Object {
+      "name": "Clara",
+    },
+  },
+}
+`;
+
 exports[`on success returns response data 1`] = `
 Object {
   "data": Object {
diff --git a/src/exchanges/fetch.test.ts b/src/exchanges/fetch.test.ts
index fc7d30f0..f6142c3e 100755
--- a/src/exchanges/fetch.test.ts
+++ b/src/exchanges/fetch.test.ts
@@ -120,6 +120,24 @@ describe('on error', () => {
 
     expect(data.operation.context).toHaveProperty('meta.networkLatency', 100);
   });
+
+  it('returns error data with status 400 and manual redirect mode', async () => {
+    const fetchOptions = jest.fn().mockReturnValue({ redirect: 'manual' });
+
+    const data = await pipe(
+      fromValue({
+        ...queryOperation,
+        context: {
+          ...queryOperation.context,
+          fetchOptions,
+        },
+      }),
+      fetchExchange(exchangeArgs),
+      toPromise
+    );
+
+    expect(data).toMatchSnapshot();
+  });
 });
 
 describe('on teardown', () => {
diff --git a/src/hooks/useDevtoolsContext.test.ts b/src/hooks/useDevtoolsContext.test.ts
new file mode 100644
index 00000000..6d2eebca
--- /dev/null
+++ b/src/hooks/useDevtoolsContext.test.ts
@@ -0,0 +1,117 @@
+import React, { Component } from 'react';
+import renderer from 'react-test-renderer';
+import { useDevtoolsContext } from './useDevtoolsContext';
+
+it('retrieves the component name for function components', () => {
+  let hookResult;
+
+  const TestFunComponent = () => {
+    hookResult = useDevtoolsContext();
+    return null;
+  };
+
+  renderer.create(React.createElement(TestFunComponent));
+
+  expect(hookResult).toEqual({
+    meta: {
+      source: 'TestFunComponent',
+    },
+  });
+});
+
+it('prefers displayNames', () => {
+  let hookResult;
+
+  const TestFunComponent = () => {
+    hookResult = useDevtoolsContext();
+    return null;
+  };
+
+  TestFunComponent.displayName = 'TestFun';
+
+  renderer.create(React.createElement(TestFunComponent));
+
+  expect(hookResult).toEqual({
+    meta: {
+      source: 'TestFun',
+    },
+  });
+});
+
+it('retrieves the component name for function components using Query', () => {
+  let hookResult;
+
+  // We're not using the actual query component here but one with the _same name_
+  const Query = () => {
+    hookResult = useDevtoolsContext();
+    return null;
+  };
+
+  const OuterFunComponent = () => {
+    return React.createElement(Query);
+  };
+
+  renderer.create(React.createElement(OuterFunComponent));
+
+  expect(hookResult).toEqual({
+    meta: {
+      source: 'OuterFunComponent',
+    },
+  });
+});
+
+it('retrieves the component name for class components using Query', () => {
+  let hookResult;
+
+  // We're not using the actual query component here but one with the _same name_
+  const Query = () => {
+    hookResult = useDevtoolsContext();
+    return null;
+  };
+
+  class OuterClassComponent extends Component {
+    render() {
+      return React.createElement(Query);
+    }
+  }
+
+  renderer.create(React.createElement(OuterClassComponent));
+
+  expect(hookResult).toEqual({
+    meta: {
+      source: 'OuterClassComponent',
+    },
+  });
+});
+
+it('looks at the parent component, not the parent fiber', () => {
+  let hookResult;
+
+  // A context consumer/provider has a different Fiber type
+  const TestContext = React.createContext('');
+
+  // We're not using the actual query component here but one with the _same name_
+  const Query = () => {
+    hookResult = useDevtoolsContext();
+    return null;
+  };
+
+  class OuterClassComponent extends Component {
+    render() {
+      // Basically put one element "in the middle" of OuterClassComponent and Query
+      return React.createElement(
+        TestContext.Provider,
+        { value: 'test' },
+        React.createElement(Query)
+      );
+    }
+  }
+
+  renderer.create(React.createElement(OuterClassComponent));
+
+  expect(hookResult).toEqual({
+    meta: {
+      source: 'OuterClassComponent',
+    },
+  });
+});
diff --git a/src/hooks/useMutation.test.tsx b/src/hooks/useMutation.test.tsx
index 321adf5a..e40de093 100644
--- a/src/hooks/useMutation.test.tsx
+++ b/src/hooks/useMutation.test.tsx
@@ -113,7 +113,7 @@ describe('on execute', () => {
       execute(vars);
     });
     expect(client.executeMutation.mock.calls[0][1]).toHaveProperty('meta', {
-      source: 'MutationUser',
+      source: 'Object',
     });
   });
 });
diff --git a/src/hooks/useQuery.test.tsx b/src/hooks/useQuery.test.tsx
index 9fa87b27..9b30bb6c 100644
--- a/src/hooks/useQuery.test.tsx
+++ b/src/hooks/useQuery.test.tsx
@@ -96,7 +96,7 @@ describe('on initial useEffect', () => {
       expect.any(Object),
       expect.objectContaining({
         meta: {
-          source: 'QueryUser',
+          source: 'Object',
         },
       })
     );
diff --git a/src/hooks/useSubscription.test.tsx b/src/hooks/useSubscription.test.tsx
index ca8efbf9..da733c2b 100644
--- a/src/hooks/useSubscription.test.tsx
+++ b/src/hooks/useSubscription.test.tsx
@@ -65,7 +65,7 @@ describe('on initial useEffect', () => {
     renderer.create(<SubscriptionUser q={query} />);
     expect(client.executeSubscription).toBeCalledWith(
       expect.any(Object),
-      expect.objectContaining({ meta: { source: 'SubscriptionUser' } })
+      expect.objectContaining({ meta: { source: 'Object' } })
     );
   });
 });