Repository: FormidableLabs/urql
Pull request #797 (2020-05-07 12:54:14)

(next-urql) - getStaticProps & getServerProps
## Summary

Relates to: https://github.com/FormidableLabs/urql/issues/754

We need to implement a switch between `getInitialProps`, `getStaticProps` and `getServerProps` I've been browsing some examples but have not yet succeeded in finding a reasonable approach. This is a first step towards some common guidelines.

## Set of changes

- Optionally bind `getInitialProps` to `withUrql`


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

Latest commit: 70ebeed4b5bbe4f4aabc24ce08346a2ba94e48fa

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

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

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

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

</details>

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

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/FormidableLabs/urql/new/support-server-props?filename=.changeset/real-buttons-worry.md&value=---%0A%22next-urql%22%3A%20patch%0A---%0A%0A(next-urql)%20-%20getStaticProps%20%26%20getServerProps%0A)


JoviDeCroock: Next up: consolidate with https://github.com/FormidableLabs/urql/pull/816

Patch:
diff --git a/.changeset/two-crews-share.md b/.changeset/two-crews-share.md
new file mode 100644
index 00000000..1aca027a
--- /dev/null
+++ b/.changeset/two-crews-share.md
@@ -0,0 +1,12 @@
+---
+'next-urql': major
+---
+
+change how `getInitialProps` is applied, this will now be applied when the page wrapped in `withUrqlClient` implements `getInitialProps` or when you
+pass it to the new second argument of `withUrqlClient`. This can be done like this:
+
+```js
+withUrqlClient(() => client, { ssr: true })(App);
+```
+
+This is to better support alternative methods like `getStaticProps`.
diff --git a/docs/advanced/server-side-rendering.md b/docs/advanced/server-side-rendering.md
index bbf103f8..f4cb68a3 100644
--- a/docs/advanced/server-side-rendering.md
+++ b/docs/advanced/server-side-rendering.md
@@ -237,3 +237,6 @@ export default withUrqlClient(ssrExchange => ({
   exchanges: [dedupExchange, cacheExchange, ssrExchange, fetchExchange],
 }))(Index);
 ```
+
+Unless the component that is being wrapped already has a `getInitialProps` method, `next-urql` won't add its own SSR logic, which automatically fetches queries during
+server-side rendering. This can be explicitly enabled by passing the `{ ssr: true }` option as a second argument to `withUrqlClient`.
diff --git a/packages/next-urql/README.md b/packages/next-urql/README.md
index 3761e1e4..b24bf4a2 100644
--- a/packages/next-urql/README.md
+++ b/packages/next-urql/README.md
@@ -127,6 +127,12 @@ When you're using `withUrqlClient` and you don't return an `exchanges` property
 
 When you yourself want to pass exchanges don't forget to include the `ssrExchange` you received as the first argument.
 
+#### `withUrqlClientOptions`
+
+The second argument for `withUrqlClient` is an options object, this contains one `boolean` property named `ssr`, you can use this to tell
+`withUrqlClient` that the wrapped component does not use `getInitialProps` but the children of this wrapped component do. This opts you into
+`ssr` for these children.
+
 ### Different Client configurations on the client and the server
 
 There are use cases where you may need different configurations for your `urql` Client on the client-side and the server-side; for example, you may want to interact with one GraphQL endpoint on the server-side and another on the client-side. `next-urql` supports this as of v0.3.0. We recommend using `typeof window === 'undefined'` or a `process.browser` check.
@@ -205,4 +211,7 @@ You can see simple example projects using `next-urql` in the `examples` director
 
 ### Caveats
 
-`withUrqlClient` implements Next's unique `getInitialProps` method under the hood. This means that any page containing a component wrapped by `withUrqlClient` will be opted out of [automatic static optimization](https://nextjs.org/docs#automatic-static-optimization). Automatic static optimization was added in Next v9, so you shouldn't worry about this if using an earlier version of Next. This is **not** unique to `next-urql` – any implementation of `getInitialProps` by any component in your application will cause Next to opt out of automatic static optimization.
+Using `withUrqlClient` on a page that has `getInitialProps` will opt that component and it's children into a prepass that does a first pass of all queries, when that
+component has children using `getInitialProps` but that component itself is not and you want to opt in to this behavior you'll have to set the second argument of
+`withUrqlClient`, this means `withUrqlClient(() => clientOptiosn, { ssr:true })`.
+This measure is available so we can support `getStaticProps`, ...
diff --git a/packages/next-urql/src/types.ts b/packages/next-urql/src/types.ts
index d693d178..c9541977 100644
--- a/packages/next-urql/src/types.ts
+++ b/packages/next-urql/src/types.ts
@@ -48,3 +48,7 @@ export interface SSRExchange extends Exchange {
   /** Extracts cached data */
   extractData(): SSRData;
 }
+
+export interface WithUrqlClientOptions {
+  ssr?: boolean;
+}
diff --git a/packages/next-urql/src/with-urql-client.tsx b/packages/next-urql/src/with-urql-client.tsx
index a16de511..15fb0eef 100644
--- a/packages/next-urql/src/with-urql-client.tsx
+++ b/packages/next-urql/src/with-urql-client.tsx
@@ -11,13 +11,22 @@ import {
 } from 'urql';
 
 import { initUrqlClient } from './init-urql-client';
-import { NextUrqlClientConfig, NextUrqlContext, WithUrqlProps } from './types';
+import {
+  NextUrqlClientConfig,
+  NextUrqlContext,
+  WithUrqlProps,
+  WithUrqlClientOptions,
+} from './types';
 
 function getDisplayName(Component: React.ComponentType<any>) {
   return Component.displayName || Component.name || 'Component';
 }
 
-export function withUrqlClient(getClientConfig: NextUrqlClientConfig) {
+export function withUrqlClient(
+  getClientConfig: NextUrqlClientConfig,
+  options?: WithUrqlClientOptions
+) {
+  if (!options) options = {};
   return (AppOrPage: NextPage<any> | typeof NextApp) => {
     const withUrql = ({ urqlClient, urqlState, ...rest }: WithUrqlProps) => {
       // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -52,63 +61,65 @@ export function withUrqlClient(getClientConfig: NextUrqlClientConfig) {
     // Set the displayName to indicate use of withUrqlClient.
     withUrql.displayName = `withUrqlClient(${getDisplayName(AppOrPage)})`;
 
-    withUrql.getInitialProps = async (appOrPageCtx: NextUrqlContext) => {
-      const { AppTree } = appOrPageCtx;
-
-      // Determine if we are wrapping an App component or a Page component.
-      const isApp = !!(appOrPageCtx as AppContext).Component;
-      const ctx = isApp
-        ? (appOrPageCtx as AppContext).ctx
-        : (appOrPageCtx as NextPageContext);
-
-      const ssrCache = ssrExchange({ initialState: undefined });
-      const clientConfig = getClientConfig(ssrCache, ctx);
-      if (!clientConfig.exchanges) {
-        // When the user does not provide exchanges we make the default assumption.
-        clientConfig.exchanges = [
-          dedupExchange,
-          cacheExchange,
-          ssrCache,
-          fetchExchange,
-        ];
-      }
-      const urqlClient = initUrqlClient(clientConfig);
-
-      if (urqlClient) {
-        (ctx as NextUrqlContext).urqlClient = urqlClient;
-      }
-
-      // Run the wrapped component's getInitialProps function.
-      let pageProps = {} as any;
-      if (AppOrPage.getInitialProps) {
-        pageProps = await AppOrPage.getInitialProps(appOrPageCtx as any);
-      }
-
-      // Check the window object to determine whether or not we are on the server.
-      // getInitialProps runs on the server for initial render, and on the client for navigation.
-      // We only want to run the prepass step on the server.
-      if (typeof window !== 'undefined') {
-        return { ...pageProps, urqlClient };
-      }
-
-      const props = { ...pageProps, urqlClient };
-      const appTreeProps = isApp ? props : { pageProps: props };
-
-      // Run the prepass step on AppTree. This will run all urql queries on the server.
-      await ssrPrepass(<AppTree {...appTreeProps} />);
-
-      // Serialize the urqlClient to null on the client-side.
-      // This ensures we don't share client and server instances of the urqlClient.
-      (urqlClient as any).toJSON = () => {
-        return null;
-      };
+    if (AppOrPage.getInitialProps || options!.ssr) {
+      withUrql.getInitialProps = async (appOrPageCtx: NextUrqlContext) => {
+        const { AppTree } = appOrPageCtx;
+
+        // Determine if we are wrapping an App component or a Page component.
+        const isApp = !!(appOrPageCtx as AppContext).Component;
+        const ctx = isApp
+          ? (appOrPageCtx as AppContext).ctx
+          : (appOrPageCtx as NextPageContext);
+
+        const ssrCache = ssrExchange({ initialState: undefined });
+        const clientConfig = getClientConfig(ssrCache, ctx);
+        if (!clientConfig.exchanges) {
+          // When the user does not provide exchanges we make the default assumption.
+          clientConfig.exchanges = [
+            dedupExchange,
+            cacheExchange,
+            ssrCache,
+            fetchExchange,
+          ];
+        }
+        const urqlClient = initUrqlClient(clientConfig);
 
-      return {
-        ...pageProps,
-        urqlState: ssrCache ? ssrCache.extractData() : undefined,
-        urqlClient,
+        if (urqlClient) {
+          (ctx as NextUrqlContext).urqlClient = urqlClient;
+        }
+
+        // Run the wrapped component's getInitialProps function.
+        let pageProps = {} as any;
+        if (AppOrPage.getInitialProps) {
+          pageProps = await AppOrPage.getInitialProps(appOrPageCtx as any);
+        }
+
+        // Check the window object to determine whether or not we are on the server.
+        // getInitialProps runs on the server for initial render, and on the client for navigation.
+        // We only want to run the prepass step on the server.
+        if (typeof window !== 'undefined') {
+          return { ...pageProps, urqlClient };
+        }
+
+        const props = { ...pageProps, urqlClient };
+        const appTreeProps = isApp ? props : { pageProps: props };
+
+        // Run the prepass step on AppTree. This will run all urql queries on the server.
+        await ssrPrepass(<AppTree {...appTreeProps} />);
+
+        // Serialize the urqlClient to null on the client-side.
+        // This ensures we don't share client and server instances of the urqlClient.
+        (urqlClient as any).toJSON = () => {
+          return null;
+        };
+
+        return {
+          ...pageProps,
+          urqlState: ssrCache ? ssrCache.extractData() : undefined,
+          urqlClient,
+        };
       };
-    };
+    }
 
     return withUrql;
   };
diff --git a/yarn.lock b/yarn.lock
index 6bdfed24..9278114a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14779,9 +14779,9 @@ websocket-driver@>=0.5.1:
     websocket-extensions ">=0.1.1"
 
 websocket-extensions@>=0.1.1:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
-  integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+  integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
 
 whatwg-encoding@^1.0.5:
   version "1.0.5"


Test patch:
diff --git a/packages/next-urql/src/__tests__/with-urql-client.spec.tsx b/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
index 74afc40d..525c78a9 100644
--- a/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
+++ b/packages/next-urql/src/__tests__/with-urql-client.spec.tsx
@@ -26,9 +26,9 @@ describe('withUrqlClient', () => {
 
   describe('with client options', () => {
     beforeEach(() => {
-      Component = withUrqlClient(() => ({ url: 'http://localhost:3000' }))(
-        MockApp
-      );
+      Component = withUrqlClient(() => ({ url: 'http://localhost:3000' }), {
+        ssr: true,
+      })(MockApp);
     });
 
     const mockContext: NextUrqlPageContext = {
@@ -82,13 +82,16 @@ describe('withUrqlClient', () => {
     };
 
     beforeEach(() => {
-      Component = withUrqlClient((ssrExchange, ctx) => ({
-        url: 'http://localhost:3000',
-        fetchOptions: {
-          headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' },
-        },
-        exchanges: [(mockSsrExchange = ssrExchange)],
-      }))(MockApp);
+      Component = withUrqlClient(
+        (ssrExchange, ctx) => ({
+          url: 'http://localhost:3000',
+          fetchOptions: {
+            headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' },
+          },
+          exchanges: [(mockSsrExchange = ssrExchange)],
+        }),
+        { ssr: true }
+      )(MockApp);
     });
 
     it('should allow a user to access the ctx object from Next on the server', async () => {
@@ -102,6 +105,35 @@ describe('withUrqlClient', () => {
     });
   });
 
+  it('should not bind getInitialProps when there are no options', async () => {
+    const mockContext: NextUrqlPageContext = {
+      AppTree: MockAppTree,
+      pathname: '/',
+      query: {},
+      asPath: '/',
+      req: {
+        headers: {
+          cookie: '',
+        },
+      } as NextUrqlPageContext['req'],
+      urqlClient: {} as Client,
+    };
+    const Component = withUrqlClient(
+      (ssrExchange, ctx) => ({
+        url: 'http://localhost:3000',
+        fetchOptions: {
+          headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' },
+        },
+        exchanges: [ssrExchange],
+      }),
+      { ssr: false }
+    )(MockApp);
+
+    Component.getInitialProps && (await Component.getInitialProps(mockContext));
+    expect(spyInitUrqlClient).toHaveBeenCalledTimes(0);
+    expect(Component.getInitialProps).toBeUndefined();
+  });
+
   describe('with exchanges provided', () => {
     const exchange = jest.fn(() => op => op);