Skip to content

Commit

Permalink
Prevent circular structure in getInitialProps. (#7)
Browse files Browse the repository at this point in the history
* Prevent circular structure in getInitialProps.

* Update TS defs to better match wrapped components that implement their own getInitialProps.

* Final edits to index.d.ts.
  • Loading branch information
parkerziegler committed Dec 18, 2019
1 parent f7cfcfe commit e1f2f3e
Show file tree
Hide file tree
Showing 12 changed files with 647 additions and 957 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['react-hooks'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
settings: {
react: {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/with-urql-client.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ describe('withUrqlClient', () => {
spyInitUrqlClient.mockClear();
});

it('should instantiate and pass the urql client instance to the wrapped component if no client is passed by getInitialProps', () => {
it('should instantiate an empty client before getInitialProps has been run', () => {
const tree = shallow(<Component />);
const app = tree.find(MockApp);

expect(app.props().urqlClient).toBeInstanceOf(Client);
expect(app.props().urqlClient.url).toEqual('http://localhost:3000');
expect(app.props().urqlClient.url).toBeUndefined();
expect(spyInitUrqlClient).toHaveBeenCalled();
});

Expand Down
6 changes: 3 additions & 3 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ See this example on [CodeSandbox](https://codesandbox.io/s/next-urql-pokedex-oqj
To get the example project running, follow these two steps:

```sh
bash install_deps.sh
yarn dev
yarn
yarn start
```

The example project should spin up at `http://localhost:3000`. `install_deps.sh` handles generating a tarball from the `src` directory to ensure proper dependency resolution for the example project and its dependencies. It also ensures that `next-urql`'s dependencies have already been installed and that the contents of the `src` directory have been built to `dist`. If you're modifying the `next-urql` `src` directory, you'll need to re-run this script to pick up changes.
The example project should spin up at `http://localhost:3000`. `yarn start` will always run the build of the `next-urql` source, so you should see changes picked up once the dev server boots up. However, if you make changes to the `next-urql` source while the dev server is running, you'll need to run `yarn start` again to see those changes take effect.
9 changes: 0 additions & 9 deletions example/install_deps.sh

This file was deleted.

13 changes: 8 additions & 5 deletions example/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ module.exports = {
webpack(config) {
config.resolve.alias.react = path.resolve(
__dirname,
'./node_modules/react',
'../node_modules/react/',
);
config.resolve.alias['react-dom'] = path.resolve(
__dirname,
'./node_modules/react-dom',
'../node_modules/react-dom/',
);
config.resolve.alias['react-is'] = path.resolve(
__dirname,
'./node_modules/react-is',
'../node_modules/react-is/',
);
config.resolve.alias.urql = path.resolve(__dirname, './node_modules/urql/');
config.output.pathinfo = true;
config.resolve.alias.urql = path.resolve(
__dirname,
'../node_modules/urql/',
);
config.resolve.alias['next-urql'] = path.resolve(__dirname, '../');
return config;
},
};
12 changes: 3 additions & 9 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next dev",
"prestart": "npm run build --prefix ../"
},
"dependencies": {
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"isomorphic-unfetch": "^3.0.0",
"next": "9.1.1",
"next-urql": "file:../next-urql.tgz",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-is": "^16.11.0",
"urql": "^1.6.1"
"next": "9.1.1"
}
}
11 changes: 10 additions & 1 deletion example/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ const Home = () => (
</div>
);

export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh' })(Home);
export default withUrqlClient(ctx => {
return {
url: 'https://graphql-pokemon.now.sh',
fetchOptions: {
headers: {
Authorization: `Bearer ${ctx.req.token}`,
},
},
};
})(Home);
1,340 changes: 513 additions & 827 deletions example/yarn.lock

Large diffs are not rendered by default.

33 changes: 13 additions & 20 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,34 @@ import { NextComponentClass, NextContext, NextFC } from 'next';
import { Client, ClientOptions, Exchange } from 'urql';
import { SSRExchange, SSRData } from 'urql/dist/types/exchanges/ssr';

type NextUrqlClientOptions = Omit<ClientOptions, 'exchanges' | 'suspense'>;

interface WithUrqlClient {
urqlClient: Client;
}

interface WithUrqlState {
interface WithUrqlInitialProps {
urqlState: SSRData;
clientOptions: NextUrqlClientOptions;
}

interface NextContextWithAppTree extends NextContext {
export interface NextContextWithAppTree extends NextContext {
AppTree: React.ComponentType<any>;
}

type NextUrqlClientOptions =
| Omit<ClientOptions, 'exchanges' | 'suspense'>
| ((
ctx: NextContext<any, any>,
) => Omit<ClientOptions, 'exchanges' | 'suspense'>);
type NextUrqlClientConfig =
| NextUrqlClientOptions
| ((ctx: NextContext<any, any>) => NextUrqlClientOptions);

declare const withUrqlClient: <T>(
declare const withUrqlClient: <T = any, IP = any>(
clientOptions: NextUrqlClientOptions,
mergeExchanges?: (ssrEx: SSRExchange) => Exchange[],
) => (
App:
| NextComponentClass<
T & WithUrqlClient,
T & WithUrqlClient,
NextContext<Record<string, string | string[] | undefined>, {}>
>
| NextFC<
T & WithUrqlClient,
T & WithUrqlClient,
NextContext<Record<string, string | string[] | undefined>, {}>
>,
| NextComponentClass<T & IP & WithUrqlClient, T & IP & WithUrqlClient>
| NextFC<T & IP & WithUrqlClient, T & IP & WithUrqlClient>,
) => NextFC<
T & WithUrqlClient & WithUrqlState,
T & WithUrqlState,
T & IP & WithUrqlClient & WithUrqlInitialProps,
IP | (IP & WithUrqlInitialProps),
NextContextWithAppTree
>;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^2.3.0",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"isomorphic-unfetch": "^3.0.0",
Expand Down
167 changes: 86 additions & 81 deletions src/with-urql-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,105 +14,110 @@ import { SSRExchange, SSRData } from 'urql/dist/types/exchanges/ssr';

import { initUrqlClient } from './init-urql-client';

type NextUrqlClientOptions = Omit<ClientOptions, 'exchanges' | 'suspense'>;

interface WithUrqlClient {
urqlClient: Client;
}

interface WithUrqlState {
interface WithUrqlInitialProps {
urqlState: SSRData;
clientOptions: NextUrqlClientOptions;
}

export interface NextContextWithAppTree extends NextContext {
AppTree: React.ComponentType<any>;
}

type NextUrqlClientOptions =
| Omit<ClientOptions, 'exchanges' | 'suspense'>
| ((
ctx: NextContext<any, any>,
) => Omit<ClientOptions, 'exchanges' | 'suspense'>);
type NextUrqlClientConfig =
| NextUrqlClientOptions
| ((ctx: NextContext<any, any>) => NextUrqlClientOptions);

const withUrqlClient = <T extends {}>(
clientOptions: NextUrqlClientOptions,
function withUrqlClient<T = any, IP = any>(
clientConfig: NextUrqlClientConfig,
mergeExchanges: (ssrEx: SSRExchange) => Exchange[] = ssrEx => [
dedupExchange,
cacheExchange,
ssrEx,
fetchExchange,
],
) => (
App: NextComponentClass<T & WithUrqlClient> | NextFC<T & WithUrqlClient>,
) => {
const withUrql: NextFC<
T & WithUrqlClient & WithUrqlState & { ctx: NextContextWithAppTree },
T & WithUrqlState,
NextContextWithAppTree
> = props => {
const opts =
typeof clientOptions === 'function'
? clientOptions(props.ctx)
: clientOptions;

const urqlClient = React.useMemo(
() =>
props.urqlClient ||
initUrqlClient(opts, mergeExchanges, props.urqlState)[0],
[],
);

return (
<Provider value={urqlClient}>
<App {...props} urqlClient={urqlClient} />
</Provider>
);
};

withUrql.getInitialProps = async (ctx: NextContextWithAppTree) => {
const { AppTree } = ctx;

// Run the wrapped component's getInitialProps function.
let appProps = {};
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx);
}

/**
* Check the window object to determine whether we are on the server.
* getInitialProps is universal, but we only want to run suspense on the server.
*/
const isBrowser = typeof window !== 'undefined';
if (isBrowser) {
return appProps as T & WithUrqlState;
}

const opts =
typeof clientOptions === 'function' ? clientOptions(ctx) : clientOptions;
const [urqlClient, ssrCache] = initUrqlClient(opts);

/**
* Run the prepass step on AppTree.
* This will run all urql queries on the server.
*/
await ssrPrepass(
<AppTree
pageProps={{
...appProps,
urqlClient: urqlClient as Client,
}}
/>,
);

// Extract the SSR query data from urql's SSR cache.
const urqlState = ssrCache && ssrCache.extractData();

return {
...appProps,
urqlState,
ctx,
} as T & WithUrqlState & { ctx: NextContextWithAppTree };
) {
return (
App:
| NextComponentClass<T & IP & WithUrqlClient, IP>
| NextFC<T & IP & WithUrqlClient, IP>,
) => {
const withUrql: NextFC<
T & IP & WithUrqlClient & WithUrqlInitialProps,
IP | (IP & WithUrqlInitialProps),
NextContextWithAppTree
> = ({ urqlClient, urqlState, clientOptions, ...rest }) => {
/**
* The React Hooks ESLint plugin will not interpret withUrql as a React component
* due to the Next.FC annotation. Ignore the warning about not using useMemo.
*/
// eslint-disable-next-line react-hooks/rules-of-hooks
const client = React.useMemo(
() =>
urqlClient ||
initUrqlClient(clientOptions, mergeExchanges, urqlState)[0],
[urqlClient, clientOptions, urqlState],
);

return (
<Provider value={client}>
<App urqlClient={client} {...(rest as T & IP)} />
</Provider>
);
};

withUrql.getInitialProps = async (ctx: NextContextWithAppTree) => {
const { AppTree } = ctx;

// Run the wrapped component's getInitialProps function.
let appProps = {} as IP;
if (App.getInitialProps) {
appProps = await App.getInitialProps(ctx);
}

/**
* Check the window object to determine whether we are on the server.
* getInitialProps is universal, but we only want to run suspense on the server.
*/
const isBrowser = typeof window !== 'undefined';
if (isBrowser) {
return appProps;
}

const opts =
typeof clientConfig === 'function' ? clientConfig(ctx) : clientConfig;
const [urqlClient, ssrCache] = initUrqlClient(opts);

/**
* Run the prepass step on AppTree.
* This will run all urql queries on the server.
*/
await ssrPrepass(
<AppTree
pageProps={{
...appProps,
urqlClient,
}}
/>,
);

// Extract the SSR query data from urql's SSR cache.
const urqlState = ssrCache && ssrCache.extractData();

return {
...appProps,
urqlState,
clientOptions: opts,
};
};

return withUrql;
};

return withUrql;
};
}

export default withUrqlClient;
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,11 @@ eslint-plugin-prettier@^3.1.1:
dependencies:
prettier-linter-helpers "^1.0.0"

eslint-plugin-react-hooks@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a"
integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw==

eslint-plugin-react@^7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz#9928e4f3e2122ed3ba6a5b56d0303ba3e41d8c09"
Expand Down

0 comments on commit e1f2f3e

Please sign in to comment.