Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

(next-urql) - streamline client creation (#816)

* write out proposal for breaking the current next-api

* refactor the non-func implementation

* fix lint

* provide fallback exchanges

* Update packages/next-urql/src/__tests__/init-urql-client.spec.ts

Co-authored-by: Parker Ziegler <parkerziegler@users.noreply.github.com>

* expand on tests

* update examples

* update docs

* add changeset and update website docs

* improve naming, clientConfig --> getClientConfig

Co-authored-by: Parker Ziegler <parkerziegler@users.noreply.github.com>

authored by

Jovi De Croock
Parker Ziegler
and committed by
GitHub
86a83436 a364e2f1

+123 -188
+26
.changeset/weak-pants-lick.md
··· 1 + --- 2 + 'next-urql': major 3 + --- 4 + 5 + Transform the `withUrqlClient` function, this removes the second argument formerly called `mergeExchanges` and merges it with the first argument. 6 + The first argument will only accept functions from now on. 7 + 8 + To migrate you would transform from: 9 + 10 + ```js 11 + export default withUrqlClient( 12 + ctx => ({ 13 + url: '', 14 + }), 15 + ssrExchange => [exchanges] 16 + ); 17 + ``` 18 + 19 + to 20 + 21 + ```js 22 + export default withUrqlClient((ssrExchange, ctx) => ({ 23 + url: '', 24 + exchanges: [exchanges], 25 + })); 26 + ```
+6 -6
docs/advanced/server-side-rendering.md
··· 213 213 // ... 214 214 }; 215 215 216 - export default withUrqlClient(ctx => ({ 216 + export default withUrqlClient((_ssrExchange, ctx) => ({ 217 217 // ...add your Client options here 218 218 url: 'http://localhost:3000/graphql', 219 219 }))(Index); ··· 225 225 226 226 One added caveat is that these options may not include the `exchanges` option because `next-urql` 227 227 injects the `ssrExchange` automatically at the right location. If you're setting up custom exchanges 228 - you'll need to instead provide them in a custom `mergeExchanges` function as the second argument: 228 + you'll need to instead provide them in the `exchanges` property of the returned client object. 229 229 230 230 ```js 231 231 import { dedupExchange, cacheExchange, fetchExchange } from '@urql/core'; 232 232 233 233 import { withUrqlClient } from 'next-urql'; 234 234 235 - // Modify this array to include custom exchanges: 236 - const mergeExchanges = ssrExchange => [dedupExchange, cacheExchange, ssrExchange, fetchExchange]; 237 - 238 - export default withUrqlClient({ url: 'http://localhost:3000/graphql' }, mergeExchanges)(Index); 235 + export default withUrqlClient(ssrExchange => ({ 236 + url: 'http://localhost:3000/graphql', 237 + exchanges: [dedupExchange, cacheExchange, ssrExchange, fetchExchange], 238 + }))(Index); 239 239 ```
+8 -26
packages/next-urql/README.md
··· 60 60 </div> 61 61 ); 62 62 63 - export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh' })(Root); 63 + export default withUrqlClient(() => ({ url: 'https://graphql-pokemon.now.sh' }))(Root); 64 64 ``` 65 65 66 66 Read more below in the [API](#API) section to learn more about the arguments that can be passed to `withUrqlClient`. ··· 78 78 The `clientOptions` argument is required. It represents all of the options you want to enable on your `urql` Client instance. It has the following union type: 79 79 80 80 ```typescript 81 - type NextUrqlClientConfig = 82 - | Omit<ClientOptions, 'exchanges' | 'suspense'> 83 - | ((ctx: NextPageContext) => Omit<ClientOptions, 'exchanges' | 'suspense'>); 81 + type NextUrqlClientConfig = (ssrExchange: SSRExchange, ctx?: NextPageContext) => ClientOptions; 84 82 ``` 85 83 86 84 The `ClientOptions` `interface` comes from `urql` itself and has the following type: ··· 93 91 fetchOptions?: RequestInit | (() => RequestInit); 94 92 /** An alternative fetch implementation. */ 95 93 fetch?: typeof fetch; 96 - /** The exchanges used by the Client. See mergeExchanges below for information on modifying exchanges in next-urql. */ 94 + /** The exchanges used by the Client. */ 97 95 exchanges?: Exchange[]; 98 96 /** A flag to enable suspense on the server. next-urql handles this for you. */ 99 97 suspense?: boolean; ··· 106 104 } 107 105 ``` 108 106 109 - This means you have two options for creating your `urql` Client. The first involves just passing the options as an object directly: 110 - 111 - ```typescript 112 - withUrqlClient({ 113 - url: 'http://localhost:3000', 114 - fetchOptions: { 115 - referrer: 'no-referrer', 116 - redirect: 'follow', 117 - }, 118 - }); 119 - ``` 120 - 121 - The second involves passing a function, which receives Next's context object, `ctx`, as an argument and returns `urql`'s Client options. This is helpful if you need to access some part of Next's context to instantiate your Client options. **Note: `ctx` is _only_ available on the initial server-side render and _not_ on client-side navigation**. This is necessary to allow for different Client configurations between server and client. 107 + You can create a client by passing a function which receives the `ssrExchange` and Next's context object, `ctx`, as arguments and returns `urql`'s Client options. This is helpful if you need to access some part of Next's context to instantiate your Client options. **Note: `ctx` is _only_ available on the initial server-side render and _not_ on client-side navigation**. This is necessary to allow for different Client configurations between server and client. 122 108 123 109 ```typescript 124 - withUrqlClient(ctx => ({ 110 + withUrqlClient((_ssrExchange, ctx) => ({ 125 111 url: 'http://localhost:3000', 126 112 fetchOptions: { 127 113 headers: { ··· 135 121 136 122 In client-side SPAs using `urql`, you typically configure the Client yourself and pass it as the `value` prop to `urql`'s context `Provider`. `withUrqlClient` handles setting all of this up for you under the hood. By default, you'll be opted into server-side `Suspense` and have the necessary `exchanges` set up for you, including the [`ssrExchange`](https://formidable.com/open-source/urql/docs/api/#ssrexchange-exchange-factory). If you need to customize your exchanges beyond the defaults `next-urql` provides, use the second argument to `withUrqlClient`, `mergeExchanges`. 137 123 138 - #### `mergeExchanges` (Optional) 124 + #### `exchanges` 139 125 140 - The `mergeExchanges` argument is optional. This is a function that takes the `ssrExchange` created by `next-urql` as its only argument and allows you to configure your exchanges as you wish. It has the following type signature: 141 - 142 - ```typescript 143 - (ssrExchange: SSRExchange) => Exchange[] 144 - ``` 126 + When you're using `withUrqlClient` and you don't return an `exchanges` property we'll assume you wanted the default exchanges, these contain: `dedupExchange`, `cacheExchange`, `ssrExchange` (the one you received as a first argument) and the `fetchExchange`. 145 127 146 - By default, `next-urql` will incorprate the `ssrExchange` into your `exchanges` array in the correct location (after any other caching exchanges, but _before_ the `fetchExchange` – read more [here](https://formidable.com/open-source/urql/docs/basics/#setting-up-the-client)). Use this argument if you want to configure your Client with additional custom `exchanges`, or access the `ssrCache` directly to extract or restore data from its cache. 128 + When you yourself want to pass exchanges don't forget to include the `ssrExchange` you received as the first argument. 147 129 148 130 ### Different Client configurations on the client and the server 149 131
+1 -1
packages/next-urql/examples/1-with-urql-client/pages/index.tsx
··· 30 30 }; 31 31 }; 32 32 33 - export default withUrqlClient((ctx: NextUrqlPageContext) => { 33 + export default withUrqlClient((_ssr: object, ctx: NextUrqlPageContext) => { 34 34 return { 35 35 url: 'https://graphql-pokemon.now.sh', 36 36 fetchOptions: {
+1 -1
packages/next-urql/examples/2-with-_app.js/pages/_app.tsx
··· 17 17 }; 18 18 }; 19 19 20 - export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh', fetch })( 20 + export default withUrqlClient(() => ({ url: 'https://graphql-pokemon.now.sh', fetch }))( 21 21 // @ts-ignore 22 22 App 23 23 );
+6 -5
packages/next-urql/examples/3-with-custom-exchange/pages/index.tsx
··· 18 18 </div> 19 19 ); 20 20 21 - export default withUrqlClient( 22 - { url: 'https://graphql-pokemon.now.sh', fetch }, 23 - (ssrExchange: SSRExchange) => [ 21 + export default withUrqlClient((ssrExchange) => ({ 22 + exchanges: [ 24 23 dedupExchange, 25 24 urlExchange, 26 25 cacheExchange, 27 26 ssrExchange, 28 27 fetchExchange, 29 - ] 30 - )(Home); 28 + ], 29 + url: 'https://graphql-pokemon.now.sh', 30 + fetch, 31 + }))(Home);
+2 -2
packages/next-urql/examples/4-with-navigation/pages/[pokemon].tsx
··· 49 49 return <h1>{result.data.pokemon.name}</h1>; 50 50 }; 51 51 52 - export default withUrqlClient({ 52 + export default withUrqlClient(() => ({ 53 53 url: 'https://graphql-pokemon.now.sh', 54 54 fetch, 55 - })(Pokemon); 55 + }))(Pokemon);
+1 -1
packages/next-urql/examples/4-with-navigation/pages/index.tsx
··· 15 15 </div> 16 16 ); 17 17 18 - export default withUrqlClient((ctx: NextUrqlPageContext) => { 18 + export default withUrqlClient((_ssr: object, ctx: NextUrqlPageContext) => { 19 19 return { 20 20 url: 'https://graphql-pokemon.now.sh', 21 21 fetchOptions: {
+6 -4
packages/next-urql/examples/5-with-suspense-exchange/pages/index.tsx
··· 18 18 </div> 19 19 ); 20 20 21 - export default withUrqlClient( 22 - { url: 'https://graphql-pokemon.now.sh', fetch, suspense: true }, 23 - (ssrExchange: SSRExchange) => [ 21 + export default withUrqlClient((ssrExchange) => ({ 22 + url: 'https://graphql-pokemon.now.sh', 23 + fetch, 24 + suspense: true, 25 + exchanges: [ 24 26 dedupExchange, 25 27 suspenseExchange, 26 28 cacheExchange, 27 29 ssrExchange, 28 30 fetchExchange, 29 31 ] 30 - )(Home); 32 + }))(Home);
+2 -53
packages/next-urql/src/__tests__/init-urql-client.spec.ts
··· 1 - import { 2 - ssrExchange, 3 - debugExchange, 4 - dedupExchange, 5 - cacheExchange, 6 - fetchExchange, 7 - } from 'urql'; 8 - 9 1 import { initUrqlClient } from '../init-urql-client'; 10 2 11 3 describe('initUrqlClient', () => { 12 - it('should return the urqlClient instance and ssrCache', () => { 13 - const [urqlClient, ssrCache] = initUrqlClient({ 4 + it('should return the urqlClient instance', () => { 5 + const urqlClient = initUrqlClient({ 14 6 url: 'http://localhost:3000', 15 7 }); 16 8 17 9 expect(urqlClient).toHaveProperty('url', 'http://localhost:3000'); 18 10 expect(urqlClient).toHaveProperty('suspense', true); 19 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 - expect(ssrCache!.toString()).toEqual(ssrExchange().toString()); 21 - }); 22 - 23 - it('should accept an optional mergeExchanges function to allow for exchange composition', () => { 24 - const [urqlClient, ssrCache] = initUrqlClient( 25 - { 26 - url: 'http://localhost:3000', 27 - }, 28 - ssrEx => [ 29 - debugExchange, 30 - dedupExchange, 31 - cacheExchange, 32 - ssrEx, 33 - fetchExchange, 34 - ] 35 - ); 36 - 37 - expect(urqlClient).toHaveProperty('url', 'http://localhost:3000'); 38 - expect(urqlClient).toHaveProperty('suspense', true); 39 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 40 - expect(ssrCache!.toString()).toEqual(ssrExchange().toString()); 41 - }); 42 - 43 - it('should accept some initial state to populate the cache', () => { 44 - const initialState = { 45 - 123: { data: { name: 'Kadabra', type: 'Psychic' } }, 46 - 456: { data: { name: 'Butterfree', type: ['Psychic', 'Bug'] } }, 47 - }; 48 - 49 - const [urqlClient, ssrCache] = initUrqlClient( 50 - { 51 - url: 'http://localhost:3000', 52 - }, 53 - undefined, 54 - initialState 55 - ); 56 - 57 - expect(urqlClient).toHaveProperty('url', 'http://localhost:3000'); 58 - expect(urqlClient).toHaveProperty('suspense', true); 59 - 60 - const data = ssrCache && ssrCache.extractData(); 61 - expect(data).toEqual(initialState); 62 11 }); 63 12 });
+24 -35
packages/next-urql/src/__tests__/with-urql-client.spec.tsx
··· 1 1 import React from 'react'; 2 2 import { shallow, configure } from 'enzyme'; 3 3 import Adapter from 'enzyme-adapter-react-16'; 4 - import { Client, defaultExchanges } from 'urql'; 4 + import { Client } from 'urql'; 5 5 6 6 import { withUrqlClient, NextUrqlPageContext } from '..'; 7 7 import * as init from '../init-urql-client'; ··· 18 18 19 19 describe('withUrqlClient', () => { 20 20 const spyInitUrqlClient = jest.spyOn(init, 'initUrqlClient'); 21 - const mockMergeExchanges = jest.fn(() => defaultExchanges); 22 21 let Component: any; 23 22 24 23 beforeAll(() => { ··· 27 26 28 27 describe('with client options', () => { 29 28 beforeEach(() => { 30 - Component = withUrqlClient({ url: 'http://localhost:3000' })(MockApp); 29 + Component = withUrqlClient(() => ({ url: 'http://localhost:3000' }))( 30 + MockApp 31 + ); 31 32 }); 32 33 33 34 const mockContext: NextUrqlPageContext = { ··· 45 46 expect(app.props().urqlClient).toBeInstanceOf(Client); 46 47 expect(app.props().urqlClient.url).toBe('http://localhost:3000'); 47 48 expect(spyInitUrqlClient).toHaveBeenCalledTimes(1); 49 + expect(spyInitUrqlClient.mock.calls[0][0].exchanges).toHaveLength(4); 48 50 }); 49 51 50 52 it('should create the urql client instance server-side inside getInitialProps', async () => { ··· 64 66 describe('with ctx callback to create client options', () => { 65 67 // Simulate a token that might be passed in a request to the server-rendered application. 66 68 const token = Math.random().toString(36).slice(-10); 69 + let mockSsrExchange; 67 70 68 71 const mockContext: NextUrqlPageContext = { 69 72 AppTree: MockAppTree, ··· 79 82 }; 80 83 81 84 beforeEach(() => { 82 - Component = withUrqlClient( 83 - ctx => ({ 84 - url: 'http://localhost:3000', 85 - fetchOptions: { 86 - headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' }, 87 - }, 88 - }), 89 - mockMergeExchanges 90 - )(MockApp); 85 + Component = withUrqlClient((ssrExchange, ctx) => ({ 86 + url: 'http://localhost:3000', 87 + fetchOptions: { 88 + headers: { Authorization: (ctx && ctx.req!.headers!.cookie) || '' }, 89 + }, 90 + exchanges: [(mockSsrExchange = ssrExchange)], 91 + }))(MockApp); 91 92 }); 92 93 93 94 it('should allow a user to access the ctx object from Next on the server', async () => { 94 95 Component.getInitialProps && 95 96 (await Component.getInitialProps(mockContext)); 96 - expect(spyInitUrqlClient).toHaveBeenCalledWith( 97 - { 98 - url: 'http://localhost:3000', 99 - fetchOptions: { headers: { Authorization: token } }, 100 - }, 101 - mockMergeExchanges 102 - ); 97 + expect(spyInitUrqlClient).toHaveBeenCalledWith({ 98 + url: 'http://localhost:3000', 99 + fetchOptions: { headers: { Authorization: token } }, 100 + exchanges: [mockSsrExchange], 101 + }); 103 102 }); 104 103 }); 105 104 106 - describe('with mergeExchanges provided', () => { 105 + describe('with exchanges provided', () => { 107 106 const exchange = jest.fn(() => op => op); 108 107 109 108 beforeEach(() => { 110 - mockMergeExchanges.mockImplementation(() => [exchange] as any[]); 111 - Component = withUrqlClient( 112 - { url: 'http://localhost:3000' }, 113 - mockMergeExchanges 114 - )(MockApp); 109 + Component = withUrqlClient(() => ({ 110 + url: 'http://localhost:3000', 111 + exchanges: [exchange] as any[], 112 + }))(MockApp); 115 113 }); 116 114 117 - it('calls the user-supplied mergeExchanges function', () => { 118 - const tree = shallow(<Component />); 119 - const app = tree.find(MockApp); 120 - 121 - const client = app.props().urqlClient; 122 - expect(client).toBeInstanceOf(Client); 123 - expect(mockMergeExchanges).toHaveBeenCalledTimes(1); 124 - }); 125 - 126 - it('uses exchanges returned from mergeExchanges', () => { 115 + it('uses exchanges defined in the client config', () => { 127 116 const tree = shallow(<Component />); 128 117 const app = tree.find(MockApp); 129 118
+3 -25
packages/next-urql/src/init-urql-client.ts
··· 1 - import { 2 - createClient, 3 - dedupExchange, 4 - cacheExchange, 5 - fetchExchange, 6 - ssrExchange, 7 - Client, 8 - Exchange, 9 - } from 'urql'; 1 + import { createClient, Client, ClientOptions } from 'urql'; 10 2 import 'isomorphic-unfetch'; 11 - import { NextUrqlClientOptions, SSRData, SSRExchange } from './types'; 12 3 13 4 let urqlClient: Client | null = null; 14 - let ssrCache: SSRExchange | null = null; 15 5 16 - export function initUrqlClient( 17 - clientOptions: NextUrqlClientOptions, 18 - mergeExchanges: (ssrEx: SSRExchange) => Exchange[] = ssrEx => [ 19 - dedupExchange, 20 - cacheExchange, 21 - ssrEx, 22 - fetchExchange, 23 - ], 24 - initialState?: SSRData 25 - ): [Client | null, SSRExchange | null] { 6 + export function initUrqlClient(clientOptions: ClientOptions): Client | null { 26 7 // Create a new Client for every server-side rendered request. 27 8 // This ensures we reset the state for each rendered page. 28 9 // If there is an exising client instance on the client-side, use it. 29 10 const isServer = typeof window === 'undefined'; 30 11 if (isServer || !urqlClient) { 31 - ssrCache = ssrExchange({ initialState }); 32 - 33 12 urqlClient = createClient({ 34 13 ...clientOptions, 35 14 suspense: isServer || clientOptions.suspense, 36 - exchanges: mergeExchanges(ssrCache), 37 15 }); 38 16 } 39 17 40 18 // Return both the Client instance and the ssrCache. 41 - return [urqlClient, ssrCache]; 19 + return urqlClient; 42 20 }
+4 -7
packages/next-urql/src/types.ts
··· 3 3 import { ClientOptions, Exchange, Client } from 'urql'; 4 4 import { AppContext } from 'next/app'; 5 5 6 - export type NextUrqlClientOptions = Omit<ClientOptions, 'exchanges'>; 7 - 8 - export type NextUrqlClientConfig = 9 - | NextUrqlClientOptions 10 - | ((ctx?: NextPageContext) => NextUrqlClientOptions); 11 - 12 - export type MergeExchanges = (ssrExchange: SSRExchange) => Exchange[]; 6 + export type NextUrqlClientConfig = ( 7 + ssrExchange: SSRExchange, 8 + ctx?: NextPageContext 9 + ) => ClientOptions; 13 10 14 11 export interface NextUrqlPageContext extends NextPageContext { 15 12 urqlClient: Client;
+33 -22
packages/next-urql/src/with-urql-client.tsx
··· 2 2 import { NextPage, NextPageContext } from 'next'; 3 3 import NextApp, { AppContext } from 'next/app'; 4 4 import ssrPrepass from 'react-ssr-prepass'; 5 - import { Provider, dedupExchange, cacheExchange, fetchExchange } from 'urql'; 5 + import { 6 + Provider, 7 + ssrExchange, 8 + dedupExchange, 9 + cacheExchange, 10 + fetchExchange, 11 + } from 'urql'; 6 12 7 13 import { initUrqlClient } from './init-urql-client'; 8 - import { 9 - NextUrqlClientConfig, 10 - MergeExchanges, 11 - NextUrqlContext, 12 - WithUrqlProps, 13 - } from './types'; 14 + import { NextUrqlClientConfig, NextUrqlContext, WithUrqlProps } from './types'; 14 15 15 16 function getDisplayName(Component: React.ComponentType<any>) { 16 17 return Component.displayName || Component.name || 'Component'; 17 18 } 18 19 19 - export function withUrqlClient( 20 - clientConfig: NextUrqlClientConfig, 21 - mergeExchanges: MergeExchanges = ssrExchange => [ 22 - dedupExchange, 23 - cacheExchange, 24 - ssrExchange, 25 - fetchExchange, 26 - ] 27 - ) { 20 + export function withUrqlClient(getClientConfig: NextUrqlClientConfig) { 28 21 return (AppOrPage: NextPage<any> | typeof NextApp) => { 29 22 const withUrql = ({ urqlClient, urqlState, ...rest }: WithUrqlProps) => { 30 23 // eslint-disable-next-line react-hooks/rules-of-hooks ··· 33 26 return urqlClient; 34 27 } 35 28 36 - const clientOptions = 37 - typeof clientConfig === 'function' ? clientConfig() : clientConfig; 29 + const ssr = ssrExchange({ initialState: urqlState }); 30 + const clientConfig = getClientConfig(ssr); 31 + if (!clientConfig.exchanges) { 32 + // When the user does not provide exchanges we make the default assumption. 33 + clientConfig.exchanges = [ 34 + dedupExchange, 35 + cacheExchange, 36 + ssr, 37 + fetchExchange, 38 + ]; 39 + } 38 40 39 41 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 40 - return initUrqlClient(clientOptions, mergeExchanges, urqlState)[0]!; 42 + return initUrqlClient(clientConfig)!; 41 43 }, [urqlClient, urqlState]); 42 44 43 45 return ( ··· 59 61 ? (appOrPageCtx as AppContext).ctx 60 62 : (appOrPageCtx as NextPageContext); 61 63 62 - const opts = 63 - typeof clientConfig === 'function' ? clientConfig(ctx) : clientConfig; 64 - const [urqlClient, ssrCache] = initUrqlClient(opts, mergeExchanges); 64 + const ssrCache = ssrExchange({ initialState: undefined }); 65 + const clientConfig = getClientConfig(ssrCache, ctx); 66 + if (!clientConfig.exchanges) { 67 + // When the user does not provide exchanges we make the default assumption. 68 + clientConfig.exchanges = [ 69 + dedupExchange, 70 + cacheExchange, 71 + ssrCache, 72 + fetchExchange, 73 + ]; 74 + } 75 + const urqlClient = initUrqlClient(clientConfig); 65 76 66 77 if (urqlClient) { 67 78 (ctx as NextUrqlContext).urqlClient = urqlClient;