Repository: FormidableLabs/urql
Pull request #358 (2019-07-31 16:42:58)

Fix useImmediateState for React Native
The `ssr` check was failing on React Native since it was kind of aggressive. I don't really want to even have it or `useLayoutEffect` in place, but multiple React threads didn't really resolve concerns that have been raised with it. Even `react-redux` uses it in this way.

I tried to find an alternative solution, but it turns out that `useState(s => s)` which would be firing at a similar time still triggers an update.

I've left a link to react-redux's repo since they're likely also be looking out for a solution to this problem.


Patch:
diff --git a/docs/api.md b/docs/api.md
index ca102454..a99ec228 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -177,6 +177,7 @@ It accepts a bunch of inputs when it's created
 | ------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
 | url          | `string`                           | The GraphQL API URL as used by `fetchExchange`                                                                  |
 | fetchOptions | `RequestInit \| () => RequestInit` | Additional `fetchOptions` that `fetch` in `fetchExchange` should use to make a request                          |
+| fetch        | `typeof fetch`                     | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch`     |
 | suspense     | `?boolean`                         | Activates the experimental React suspense mode, which can be used during server-side rendering to prefetch data |
 | exchanges    | `Exchange[]`                       | An array of `Exchange`s that the client should use instead of the list of `defaultExchanges`                    |
 
diff --git a/src/client.ts b/src/client.ts
index 75827692..e35263a7 100755
--- a/src/client.ts
+++ b/src/client.ts
@@ -33,6 +33,8 @@ export interface ClientOptions {
   url: string;
   /** Any additional options to pass to fetch. */
   fetchOptions?: RequestInit | (() => RequestInit);
+  /** An alternative fetch implementation. */
+  fetch?: typeof fetch;
   /** An ordered array of Exchanges. */
   exchanges?: Exchange[];
   /** Activates support for Suspense. */
@@ -49,6 +51,7 @@ export const createClient = (opts: ClientOptions) => new Client(opts);
 export class Client {
   // These are variables derived from ClientOptions
   url: string;
+  fetch?: typeof fetch;
   fetchOptions?: RequestInit | (() => RequestInit);
   exchange: Exchange;
   suspense: boolean;
@@ -62,6 +65,7 @@ export class Client {
   constructor(opts: ClientOptions) {
     this.url = opts.url;
     this.fetchOptions = opts.fetchOptions;
+    this.fetch = opts.fetch;
     this.suspense = !!opts.suspense;
 
     // This subject forms the input of operations; executeOperation may be
diff --git a/src/exchanges/fetch.ts b/src/exchanges/fetch.ts
index f8ff9c1f..cc381855 100755
--- a/src/exchanges/fetch.ts
+++ b/src/exchanges/fetch.ts
@@ -76,6 +76,7 @@ const createFetchSource = (operation: Operation) => {
     };
 
     const startTime = Date.now();
+
     executeFetch(operation, fetchOptions).then(result => {
       if (result !== undefined) {
         next({
@@ -99,13 +100,13 @@ const createFetchSource = (operation: Operation) => {
 
 const executeFetch = (operation: Operation, opts: RequestInit) => {
   let response: Response | undefined;
-  const { url } = operation.context;
+  const { url, fetch: fetcher } = operation.context;
 
-  return fetch(url, opts)
+  return (fetcher || fetch)(url, opts)
     .then(res => {
       response = res;
-      checkStatus(opts.redirect, response);
-      return response.json();
+      checkStatus(opts.redirect, res);
+      return res.json();
     })
     .then(result => ({
       operation,
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 6021f177..49861b08 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,7 +1,6 @@
 export * from './error';
 export * from './keyForQuery';
 export * from './request';
-export * from './ssr';
 export * from './typenames';
 export * from './toSuspenseSource';
 
diff --git a/src/utils/ssr.ts b/src/utils/ssr.ts
deleted file mode 100644
index 9eef157d..00000000
--- a/src/utils/ssr.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export const isSSR =
-  typeof window === 'undefined' || !('HTMLElement' in window);


Test patch:
diff --git a/src/__snapshots__/client.test.ts.snap b/src/__snapshots__/client.test.ts.snap
index cff7bd7e..4885bab4 100644
--- a/src/__snapshots__/client.test.ts.snap
+++ b/src/__snapshots__/client.test.ts.snap
@@ -10,6 +10,7 @@ Client {
   "executeMutation": [Function],
   "executeQuery": [Function],
   "executeSubscription": [Function],
+  "fetch": undefined,
   "fetchOptions": undefined,
   "operations$": [Function],
   "reexecuteOperation": [Function],
diff --git a/src/__snapshots__/context.test.ts.snap b/src/__snapshots__/context.test.ts.snap
index 64a38f79..3a40f8a5 100644
--- a/src/__snapshots__/context.test.ts.snap
+++ b/src/__snapshots__/context.test.ts.snap
@@ -23,6 +23,7 @@ Object {
       "executeMutation": [Function],
       "executeQuery": [Function],
       "executeSubscription": [Function],
+      "fetch": undefined,
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
@@ -39,6 +40,7 @@ Object {
       "executeMutation": [Function],
       "executeQuery": [Function],
       "executeSubscription": [Function],
+      "fetch": undefined,
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
@@ -74,6 +76,7 @@ Object {
       "executeMutation": [Function],
       "executeQuery": [Function],
       "executeSubscription": [Function],
+      "fetch": undefined,
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
@@ -90,6 +93,7 @@ Object {
       "executeMutation": [Function],
       "executeQuery": [Function],
       "executeSubscription": [Function],
+      "fetch": undefined,
       "fetchOptions": undefined,
       "operations$": [Function],
       "reexecuteOperation": [Function],
diff --git a/src/hooks/useImmediateState.ts b/src/hooks/useImmediateState.ts
index 979d9524..701a2aa1 100644
--- a/src/hooks/useImmediateState.ts
+++ b/src/hooks/useImmediateState.ts
@@ -1,9 +1,18 @@
-import { useRef, useState, useCallback, useLayoutEffect } from 'react';
-import { isSSR } from '../utils';
+import {
+  useRef,
+  useState,
+  useCallback,
+  useEffect,
+  useLayoutEffect,
+} from 'react';
 
 type SetStateAction<S> = S | ((prevState: S) => S);
 type SetState<S> = (action: SetStateAction<S>) => void;
 
+// See https://github.com/reduxjs/react-redux/blob/316467a/src/hooks/useSelector.js#L6-L15
+const useIsomorphicLayoutEffect =
+  typeof window !== 'undefined' ? useLayoutEffect : useEffect;
+
 /**
  * This is a drop-in replacement for useState, limited to object-based state.
  * During initial mount it will mutably update the state, instead of scheduling
@@ -11,32 +20,31 @@ type SetState<S> = (action: SetStateAction<S>) => void;
  */
 export const useImmediateState = <S extends {}>(init: S): [S, SetState<S>] => {
   const isMounted = useRef(false);
-  const initialState = useRef<S>({ ...init });
-  const [state, setState] = useState<S>(initialState.current);
+  const [state, setState] = useState<S>(init);
 
   // This wraps setState and updates the state mutably on initial mount
   const updateState: SetState<S> = useCallback(
     (action: SetStateAction<S>): void => {
       if (!isMounted.current) {
-        const newValue =
+        const newState =
           typeof action === 'function'
-            ? (action as (arg: S) => S)(initialState.current)
+            ? (action as (arg: S) => S)(state)
             : action;
-        Object.assign(initialState.current, newValue);
+        Object.assign(state, newState);
       } else {
         setState(action);
       }
     },
+    // eslint-disable-next-line react-hooks/exhaustive-deps
     []
   );
 
-  !isSSR && // eslint-disable-next-line react-hooks/rules-of-hooks
-    useLayoutEffect(() => {
-      isMounted.current = true;
-      return () => {
-        isMounted.current = false;
-      };
-    }, []);
+  useIsomorphicLayoutEffect(() => {
+    isMounted.current = true;
+    return () => {
+      isMounted.current = false;
+    };
+  }, []);
 
   return [state, updateState];
 };