Repository: FormidableLabs/urql
Pull request #904 (2020-07-16 06:24:02)

(react-hooks) prefer exported function instead of block-scoped variable
## Summary

This PR changes the definition of React Hooks to use `export function` instead of `export const`. It's a simple change that allow developers later to interact better with the signature of the hooks.

In TS, these `const` are defined as block-scoped variables, and cannot be overwritten/redefined/overloaded by external TS libraries or consumers. 

You can see a [real-time example for that here](https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=12&pc=2#code/CYUwxgNghgTiAEBbA9sArhBByALiAzjlvAN4CwAUPNfAPS3wgAeADsjDvAGZoB2YOAJbJe8AO7sA1vko1Grdpx78hIpAE8AYnzAAKFrCiIAXPEIxBvAOYBKU+ctWA3JVk168th3hgRheMDIBLy44lJu1MxenL68-ojqAGqwpvqGJmY4FtY28AC8AHyZ2c6UAL6uFKCQsAgo6JjwuAREpBGeitw6qqIJ2vxpMEamvGiIAEYgMHbwoxNTLlQ0UZ2x8Ukp8IPDs2OT0-lFc-uLZUA)  , and it's [also reported as issue](https://github.com/microsoft/TypeScript/issues/39622) 

I'm working on a new plugin for https://github.com/dotansimha/graphql-code-generator , and I would like to extend `urql` behaviour with module augmentation - but at the moment it's not possible because of the way the hooks are defined.

## Set of changes

Changed `export const` to be `export function` in all React Hooks.


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

Latest commit: 559d98ee3c042c6dd072fef861d862f5ad860f43

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

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

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

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

</details>

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

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/dotansimha/urql/new/prefer-function?filename=.changeset/brave-terms-listen.md&value=---%0A%22urql%22%3A%20patch%0A---%0A%0A(react-hooks)%20prefer%20exported%20function%20instead%20of%20block-scoped%20variable%0A)


dotansimha: @JoviDeCroock `changeset` seems to work fine now, thank you for fixing it :) I updated the PR. 


Patch:
diff --git a/.changeset/six-shoes-kneel.md b/.changeset/six-shoes-kneel.md
new file mode 100644
index 00000000..29ecbf25
--- /dev/null
+++ b/.changeset/six-shoes-kneel.md
@@ -0,0 +1,5 @@
+---
+'urql': patch
+---
+
+prefer exported function instead of block-scoped variable
diff --git a/docs/concepts/exchanges.md b/docs/concepts/exchanges.md
index 88c6301c..a792eb9b 100644
--- a/docs/concepts/exchanges.md
+++ b/docs/concepts/exchanges.md
@@ -25,7 +25,8 @@ Other available exchanges:
 - `retryExchange`: Allows operations to be retried
 - `devtoolsExchange`: Provides the ability to use the [urql-devtools](https://github.com/FormidableLabs/urql-devtools)
 - `multipartFetchExchange`: Provides multipart file upload capability
-- `suspenseExchange` (experimental): Allows the use of React Suspense on the client-side with `urql`'s built-in suspense mode
+- `suspenseExchange` (experimental): Allows the use of React Suspense on the client-side with `urql`'s built-in suspense modeµ
+- `requestPolicyExchange`: Automatically upgrades `cache-only` and `cache-first` operations to `cache-and-network` after a given amount of time.
 
 It is also possible to apply custom exchanges to override the default logic.
 
diff --git a/exchanges/request-policy/CHANGELOG.md b/exchanges/request-policy/CHANGELOG.md
new file mode 100644
index 00000000..8c30f86a
--- /dev/null
+++ b/exchanges/request-policy/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## v0.1.0
+
+**Initial Release**
diff --git a/exchanges/request-policy/README.md b/exchanges/request-policy/README.md
new file mode 100644
index 00000000..020d438f
--- /dev/null
+++ b/exchanges/request-policy/README.md
@@ -0,0 +1,39 @@
+# @urql/exchange-request-policy (Exchange factory)
+
+`@urql/exchange-request-policy` is an exchange for the [`urql`](../../README.md) GraphQL client that will automatically upgrade operation request-policies
+on a time-to-live basis.
+
+## Quick Start Guide
+
+First install `@urql/exchange-request-policy` alongside `urql`:
+
+```sh
+yarn add @urql/exchange-request-policy
+# or
+npm install --save @urql/exchange-request-policy
+```
+
+Then add it to your client.
+
+```js
+import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
+import { requestPolicyExchange } from '@urql/exchange-request-policy';
+
+const client = createClient({
+  url: 'http://localhost:1234/graphql',
+  exchanges: [
+    dedupExchange,
+    requestPolicyExchange({
+      // The amount of time in ms that has to go by before upgrading, default is 5 minutes.
+      ttl: 60 * 1000, // 1 minute.
+      // An optional function that allows you to specify whether an operation should be upgraded.
+      shouldUpgrade: operation => operation.context.requestPolicy !== 'cache-only',
+    }),
+    cacheExchange,
+    fetchExchange,
+  ],
+});
+```
+
+Now when the exchange sees a `cache-first` operation that hasn't been seen in ttl amount of time it will upgrade
+the `requestPolicy` to `cache-and-network`.
diff --git a/exchanges/request-policy/package.json b/exchanges/request-policy/package.json
new file mode 100644
index 00000000..b857bae3
--- /dev/null
+++ b/exchanges/request-policy/package.json
@@ -0,0 +1,66 @@
+{
+  "name": "@urql/exchange-request-policy",
+  "version": "0.1.0",
+  "description": "An exchange for operation request-policy upgrading in urql",
+  "sideEffects": false,
+  "homepage": "https://formidable.com/open-source/urql/docs/",
+  "bugs": "https://github.com/FormidableLabs/urql/issues",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/FormidableLabs/urql.git",
+    "directory": "exchanges/request-policy"
+  },
+  "keywords": [
+    "urql",
+    "graphql client",
+    "formidablelabs",
+    "exchanges",
+    "react",
+    "request-policy"
+  ],
+  "main": "dist/urql-exchange-request-policy",
+  "module": "dist/urql-exchange-request-policy.mjs",
+  "types": "dist/types/index.d.ts",
+  "source": "src/index.ts",
+  "exports": {
+    ".": {
+      "import": "./dist/urql-exchange-request-policy.mjs",
+      "require": "./dist/urql-exchange-request-policy.js",
+      "types": "./dist/types/index.d.ts",
+      "source": "./src/index.ts"
+    },
+    "./package.json": "./package.json"
+  },
+  "files": [
+    "LICENSE",
+    "CHANGELOG.md",
+    "README.md",
+    "dist/"
+  ],
+  "scripts": {
+    "test": "jest",
+    "clean": "rimraf dist",
+    "check": "tsc --noEmit",
+    "lint": "eslint --ext=js,jsx,ts,tsx .",
+    "build": "rollup -c ../../scripts/rollup/config.js",
+    "prepare": "node ../../scripts/prepare/index.js",
+    "prepublishOnly": "run-s clean build"
+  },
+  "jest": {
+    "preset": "../../scripts/jest/preset"
+  },
+  "devDependencies": {
+    "graphql": "^15.1.0"
+  },
+  "peerDependencies": {
+    "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
+  },
+  "dependencies": {
+    "@urql/core": ">=1.12.0",
+    "wonka": "^4.0.14"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}
diff --git a/exchanges/request-policy/src/index.ts b/exchanges/request-policy/src/index.ts
new file mode 100644
index 00000000..45669158
--- /dev/null
+++ b/exchanges/request-policy/src/index.ts
@@ -0,0 +1,1 @@
+export { requestPolicyExchange } from './requestPolicyExchange';
diff --git a/exchanges/request-policy/src/requestPolicyExchange.ts b/exchanges/request-policy/src/requestPolicyExchange.ts
new file mode 100644
index 00000000..33bed890
--- /dev/null
+++ b/exchanges/request-policy/src/requestPolicyExchange.ts
@@ -0,0 +1,51 @@
+import { Operation, Exchange } from '@urql/core';
+import { pipe, map } from 'wonka';
+
+const defaultTTL = 5 * 60 * 1000;
+
+export interface Options {
+  shouldUpgrade?: (op: Operation) => boolean;
+  ttl?: number;
+}
+
+export const requestPolicyExchange = (options: Options): Exchange => ({
+  forward,
+}) => {
+  const operations = new Map();
+
+  const TTL = (options || {}).ttl || defaultTTL;
+
+  const processIncomingOperation = (operation: Operation): Operation => {
+    if (
+      operation.context.requestPolicy !== 'cache-first' &&
+      operation.context.requestPolicy !== 'cache-only'
+    )
+      return operation;
+    if (!operations.has(operation.key)) {
+      operations.set(operation.key, new Date());
+      return operation;
+    }
+
+    const lastOccurrence = operations.get(operation.key);
+    const currentTime = new Date().getTime();
+    if (
+      currentTime - lastOccurrence.getTime() > TTL &&
+      (options.shouldUpgrade ? options.shouldUpgrade(operation) : true)
+    ) {
+      operations.set(operation.key, new Date());
+      return {
+        ...operation,
+        context: {
+          ...operation.context,
+          requestPolicy: 'cache-and-network',
+        },
+      };
+    }
+
+    return operation;
+  };
+
+  return ops$ => {
+    return forward(pipe(ops$, map(processIncomingOperation)));
+  };
+};
diff --git a/exchanges/request-policy/tsconfig.json b/exchanges/request-policy/tsconfig.json
new file mode 100644
index 00000000..5797ce61
--- /dev/null
+++ b/exchanges/request-policy/tsconfig.json
@@ -0,0 +1,13 @@
+{
+  "extends": "../../tsconfig.json",
+  "include": ["src"],
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "urql": ["../../node_modules/urql/src"],
+      "*-urql": ["../../node_modules/*-urql/src"],
+      "@urql/core/*": ["../../node_modules/@urql/core/src/*"],
+      "@urql/*": ["../../node_modules/@urql/*/src"]
+    }
+  }
+}
diff --git a/packages/react-urql/src/hooks/useMutation.ts b/packages/react-urql/src/hooks/useMutation.ts
index 0b59c492..bfd543b2 100644
--- a/packages/react-urql/src/hooks/useMutation.ts
+++ b/packages/react-urql/src/hooks/useMutation.ts
@@ -28,9 +28,9 @@ export type UseMutationResponse<T, V> = [
   ) => Promise<OperationResult<T>>
 ];
 
-export const useMutation = <T = any, V = object>(
+export function useMutation<T = any, V = object>(
   query: DocumentNode | string
-): UseMutationResponse<T, V> => {
+): UseMutationResponse<T, V> {
   const isMounted = useRef(true);
   const client = useClient();
 
@@ -69,4 +69,4 @@ export const useMutation = <T = any, V = object>(
   }, []);
 
   return [state, executeMutation];
-};
+}
diff --git a/packages/react-urql/src/hooks/useQuery.ts b/packages/react-urql/src/hooks/useQuery.ts
index 6c67f1f0..e2410ce4 100644
--- a/packages/react-urql/src/hooks/useQuery.ts
+++ b/packages/react-urql/src/hooks/useQuery.ts
@@ -30,9 +30,9 @@ export type UseQueryResponse<T> = [
   (opts?: Partial<OperationContext>) => void
 ];
 
-export const useQuery = <T = any, V = object>(
+export function useQuery<T = any, V = object>(
   args: UseQueryArgs<V>
-): UseQueryResponse<T> => {
+): UseQueryResponse<T> {
   const client = useClient();
 
   // This creates a request which will keep a stable reference
@@ -100,4 +100,4 @@ export const useQuery = <T = any, V = object>(
   );
 
   return [state, executeQuery];
-};
+}
diff --git a/packages/react-urql/src/hooks/useRequest.ts b/packages/react-urql/src/hooks/useRequest.ts
index 22fff7e2..a6414e23 100644
--- a/packages/react-urql/src/hooks/useRequest.ts
+++ b/packages/react-urql/src/hooks/useRequest.ts
@@ -3,10 +3,10 @@ import { useRef, useMemo } from 'react';
 import { GraphQLRequest, createRequest } from '@urql/core';
 
 /** Creates a request from a query and variables but preserves reference equality if the key isn't changing */
-export const useRequest = (
+export function useRequest(
   query: string | DocumentNode,
   variables?: any
-): GraphQLRequest => {
+): GraphQLRequest {
   const prev = useRef<undefined | GraphQLRequest>(undefined);
 
   return useMemo(() => {
@@ -19,4 +19,4 @@ export const useRequest = (
       return request;
     }
   }, [query, variables]);
-};
+}
diff --git a/packages/react-urql/src/hooks/useSource.ts b/packages/react-urql/src/hooks/useSource.ts
index ffc9a093..7d81b116 100644
--- a/packages/react-urql/src/hooks/useSource.ts
+++ b/packages/react-urql/src/hooks/useSource.ts
@@ -18,7 +18,7 @@ import { useClient } from '../context';
 
 let currentInit = false;
 
-export const useSource = <T>(source: Source<T>, init: T): T => {
+export function useSource<T>(source: Source<T>, init: T): T {
   const isMounted = useRef(true);
 
   const [state, setState] = useState(() => {
@@ -55,9 +55,9 @@ export const useSource = <T>(source: Source<T>, init: T): T => {
   }, [source]);
 
   return state;
-};
+}
 
-export const useBehaviourSubject = <T>(value: T) => {
+export function useBehaviourSubject<T>(value: T) {
   const client = useClient();
 
   const state = useMemo((): [Source<T>, (value: T) => void] => {
@@ -93,4 +93,4 @@ export const useBehaviourSubject = <T>(value: T) => {
   if (client.suspense) state[1](value);
 
   return state;
-};
+}
diff --git a/packages/react-urql/src/hooks/useSubscription.ts b/packages/react-urql/src/hooks/useSubscription.ts
index d6b6bff4..be42d67f 100644
--- a/packages/react-urql/src/hooks/useSubscription.ts
+++ b/packages/react-urql/src/hooks/useSubscription.ts
@@ -30,10 +30,10 @@ export type UseSubscriptionResponse<T> = [
   (opts?: Partial<OperationContext>) => void
 ];
 
-export const useSubscription = <T = any, R = T, V = object>(
+export function useSubscription<T = any, R = T, V = object>(
   args: UseSubscriptionArgs<V>,
   handler?: SubscriptionHandler<T, R>
-): UseSubscriptionResponse<R> => {
+): UseSubscriptionResponse<R> {
   const client = useClient();
 
   // Update handler on constant ref, since handler changes shouldn't
@@ -108,4 +108,4 @@ export const useSubscription = <T = any, R = T, V = object>(
   );
 
   return [state, executeSubscription];
-};
+}
diff --git a/packages/site/package.json b/packages/site/package.json
index bdefe8f3..c91435d8 100644
--- a/packages/site/package.json
+++ b/packages/site/package.json
@@ -53,7 +53,7 @@
   "devDependencies": {
     "@babel/core": "^7.2.0",
     "@mdx-js/mdx": "^1.5.7",
-    "lodash": "^4.17.11",
+    "lodash": "^4.17.19",
     "react-hot-loader": "^4.12.20",
     "react-static-plugin-react-router": "^7.2.3",
     "react-static-plugin-sitemap": "^7.0.0",
diff --git a/yarn.lock b/yarn.lock
index f082e9bd..2db26971 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8851,10 +8851,10 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.5:
-  version "4.17.15"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
-  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.5:
+  version "4.17.19"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
 
 log-symbols@^4.0.0:
   version "4.0.0"


Test patch:
diff --git a/exchanges/request-policy/src/requestPolicyExchange.test.ts b/exchanges/request-policy/src/requestPolicyExchange.test.ts
new file mode 100644
index 00000000..e0022e0d
--- /dev/null
+++ b/exchanges/request-policy/src/requestPolicyExchange.test.ts
@@ -0,0 +1,136 @@
+import gql from 'graphql-tag';
+
+import { pipe, map, makeSubject, publish, tap } from 'wonka';
+
+import {
+  createClient,
+  Operation,
+  OperationResult,
+  ExchangeIO,
+} from '@urql/core';
+import { requestPolicyExchange } from './requestPolicyExchange';
+
+const dispatchDebug = jest.fn();
+
+const mockOptions = {
+  ttl: 5,
+};
+
+const queryOne = gql`
+  {
+    author {
+      id
+      name
+    }
+  }
+`;
+
+const queryOneData = {
+  __typename: 'Query',
+  author: {
+    __typename: 'Author',
+    id: '123',
+    name: 'Author',
+  },
+};
+
+let client, op, ops$, next;
+beforeEach(() => {
+  client = createClient({ url: 'http://0.0.0.0' });
+  op = client.createRequestOperation('query', {
+    key: 1,
+    query: queryOne,
+  });
+
+  ({ source: ops$, next } = makeSubject<Operation>());
+});
+
+it(`upgrades to cache-and-network`, done => {
+  const response = jest.fn(
+    (forwardOp: Operation): OperationResult => {
+      return {
+        operation: forwardOp,
+        data: queryOneData,
+      };
+    }
+  );
+
+  const result = jest.fn();
+  const forward: ExchangeIO = ops$ => {
+    return pipe(ops$, map(response));
+  };
+
+  pipe(
+    requestPolicyExchange(mockOptions)({
+      forward,
+      client,
+      dispatchDebug,
+    })(ops$),
+    tap(result),
+    publish
+  );
+
+  next(op);
+
+  expect(response).toHaveBeenCalledTimes(1);
+  expect(response.mock.calls[0][0].context.requestPolicy).toEqual(
+    'cache-first'
+  );
+  expect(result).toHaveBeenCalledTimes(1);
+
+  setTimeout(() => {
+    next(op);
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(response.mock.calls[1][0].context.requestPolicy).toEqual(
+      'cache-and-network'
+    );
+    expect(result).toHaveBeenCalledTimes(2);
+    done();
+  }, 10);
+});
+
+it(`doesn't upgrade when shouldUpgrade returns false`, done => {
+  const response = jest.fn(
+    (forwardOp: Operation): OperationResult => {
+      return {
+        operation: forwardOp,
+        data: queryOneData,
+      };
+    }
+  );
+
+  const result = jest.fn();
+  const forward: ExchangeIO = ops$ => {
+    return pipe(ops$, map(response));
+  };
+
+  const shouldUpgrade = jest.fn(() => false);
+  pipe(
+    requestPolicyExchange({ ...mockOptions, shouldUpgrade })({
+      forward,
+      client,
+      dispatchDebug,
+    })(ops$),
+    tap(result),
+    publish
+  );
+
+  next(op);
+
+  expect(response).toHaveBeenCalledTimes(1);
+  expect(response.mock.calls[0][0].context.requestPolicy).toEqual(
+    'cache-first'
+  );
+  expect(result).toHaveBeenCalledTimes(1);
+
+  setTimeout(() => {
+    next(op);
+    expect(response).toHaveBeenCalledTimes(2);
+    expect(response.mock.calls[1][0].context.requestPolicy).toEqual(
+      'cache-first'
+    );
+    expect(result).toHaveBeenCalledTimes(2);
+    expect(shouldUpgrade).toBeCalledTimes(1);
+    done();
+  }, 10);
+});