Skip to content
This repository has been archived by the owner on Mar 8, 2020. It is now read-only.

Add better support for _app.js. Move types to separate module. #38

Merged
merged 3 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions examples/1-with-urql-client/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import React from 'react';
import { NextComponentType } from 'next';
import Head from 'next/head';
import { withUrqlClient, NextUrqlContext } from 'next-urql';
import { withUrqlClient, NextUrqlPageContext } from 'next-urql';
import PokémonList from '../components/pokemon_list';

const Home: React.FC = () => (
interface InitialProps {
title: string;
}

const Home: NextComponentType<
NextUrqlPageContext,
InitialProps,
InitialProps
> = ({ title }) => (
<div>
<Head>
<title>Home</title>
<link rel="icon" href="/static/favicon.ico" />
</Head>

<h1>{title}</h1>
<PokémonList />
</div>
);

export default withUrqlClient((ctx: NextUrqlContext) => {
Home.getInitialProps = () => {
return {
title: 'Pokédex',
};
};

export default withUrqlClient((ctx: NextUrqlPageContext) => {
return {
url: 'https://graphql-pokemon.now.sh',
fetchOptions: {
headers: {
Authorization: `Bearer ${ctx.req.headers.authorization}`,
Authorization: `Bearer ${ctx?.req?.headers?.authorization ?? ''}`,
},
},
};
Expand Down
8 changes: 4 additions & 4 deletions examples/2-with-_app.js/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { withUrqlClient } from 'next-urql';
import { AppPropsType } from 'next/dist/next-server/lib/utils';
import { AppProps } from 'next/app';

const App: React.FC<AppPropsType> = ({ Component, pageProps }) => (
<Component {...pageProps} />
);
const App = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};

export default withUrqlClient({ url: 'https://graphql-pokemon.now.sh' })(App);
31 changes: 22 additions & 9 deletions examples/2-with-_app.js/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import React from 'react';
import { NextPage } from 'next';
import Head from 'next/head';

import PokémonList from '../components/pokemon_list';

const Home: React.FC = () => (
<div>
<Head>
<title>Home</title>
<link rel="icon" href="/favicon.ico" />
</Head>
interface InitialProps {
title: string;
}

<PokémonList />
</div>
);
const Home: NextPage<InitialProps> = ({ title }) => {
return (
<div>
<Head>
<title>Home</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<h1>{title}</h1>
<PokémonList />
</div>
);
};

Home.getInitialProps = () => {
return {
title: 'Pokédex',
};
};

export default Home;
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as withUrqlClient, NextUrqlContext } from './with-urql-client';
export { withUrqlClient } from './with-urql-client';
export * from './types';
11 changes: 4 additions & 7 deletions src/init-urql-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import {
Exchange,
} from 'urql';
import { SSRData, SSRExchange } from 'urql/dist/types/exchanges/ssr';

import 'isomorphic-unfetch';

import { NextUrqlClientOptions } from './with-urql-client';
import { NextUrqlClientOptions } from './types';

let urqlClient: Client | null = null;
let ssrCache: SSRExchange | null = null;
Expand All @@ -26,11 +25,9 @@ export function initUrqlClient(
],
initialState?: SSRData,
): [Client | null, SSRExchange | null] {
/**
* Create a new Client for every server-side rendered request.
* This ensures we reset the state for each rendered page.
* If there is an exising client instance on the client-side, use it.
*/
// Create a new Client for every server-side rendered request.
// This ensures we reset the state for each rendered page.
// If there is an exising client instance on the client-side, use it.
const isServer = typeof window === 'undefined';
if (isServer || !urqlClient) {
ssrCache = ssrExchange({ initialState });
Expand Down
37 changes: 37 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextPageContext } from 'next';
import { ClientOptions, Exchange, Client } from 'urql';
import { SSRExchange, SSRData } from 'urql/dist/types/exchanges/ssr';
import { AppContext } from 'next/app';

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

export type NextUrqlClientConfig =
| NextUrqlClientOptions
| ((ctx?: NextPageContext) => NextUrqlClientOptions);

export type MergeExchanges = (ssrExchange: SSRExchange) => Exchange[];

export interface NextUrqlPageContext extends NextPageContext {
urqlClient: Client;
}

export interface NextUrqlAppContext extends AppContext {
urqlClient: Client;
}

export type NextUrqlContext = NextUrqlPageContext | NextUrqlAppContext;

export interface WithUrqlState {
urqlState?: SSRData;
}

export interface WithUrqlClient {
urqlClient: Client;
}

export interface WithUrqlProps extends WithUrqlClient, WithUrqlState {
[key: string]: any;
}
116 changes: 38 additions & 78 deletions src/with-urql-client.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,75 @@
import React from 'react';
import { NextPage, NextPageContext, NextComponentType } from 'next';
import { AppContext } from 'next/app';
import { NextPage, NextPageContext } from 'next';
import NextApp, { AppContext } from 'next/app';
import ssrPrepass from 'react-ssr-prepass';
import {
Provider,
Client,
ClientOptions,
dedupExchange,
cacheExchange,
fetchExchange,
Exchange,
} from 'urql';
import { SSRExchange, SSRData } from 'urql/dist/types/exchanges/ssr';
import { Provider, dedupExchange, cacheExchange, fetchExchange } from 'urql';

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

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

interface WithUrqlClient {
urqlClient?: Client;
}

interface WithUrqlInitialProps {
urqlState: SSRData;
}

interface PageProps {
pageProps?: WithUrqlClient;
}

export interface NextUrqlContext extends NextPageContext {
urqlClient: Client;
import {
NextUrqlClientConfig,
MergeExchanges,
NextUrqlContext,
WithUrqlProps,
} from './types';

function getDisplayName(Component: React.ComponentType<any>) {
return Component.displayName || Component.name || 'Component';
}

type NextUrqlClientConfig =
| NextUrqlClientOptions
| ((ctx?: NextPageContext) => NextUrqlClientOptions);

function withUrqlClient<T = any, IP = any>(
export function withUrqlClient(
clientConfig: NextUrqlClientConfig,
mergeExchanges: (ssrEx: SSRExchange) => Exchange[] = ssrEx => [
mergeExchanges: MergeExchanges = ssrExchange => [
dedupExchange,
cacheExchange,
ssrEx,
ssrExchange,
fetchExchange,
],
) {
return (Page: NextPage<T & IP & WithUrqlClient, IP>) => {
const withUrql: NextComponentType<
NextUrqlContext,
IP | (IP & WithUrqlInitialProps),
T & IP & WithUrqlClient & WithUrqlInitialProps & PageProps
> = ({ urqlClient, urqlState, pageProps, ...rest }) => {
// The React Hooks ESLint plugin will not interpret withUrql as a React component
// due to the NextComponentType annotation. Ignore the warning about not using useMemo.

return (AppOrPage: NextPage<any> | typeof NextApp) => {
const withUrql = ({ urqlClient, urqlState, ...rest }: WithUrqlProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const client = React.useMemo(() => {
const clientOptions =
typeof clientConfig === 'function' ? clientConfig() : clientConfig;

return (
urqlClient ||
pageProps?.urqlClient ||
initUrqlClient(clientOptions, mergeExchanges, urqlState)[0]
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
initUrqlClient(clientOptions, mergeExchanges, urqlState)[0]!
);
}, [urqlClient, pageProps, urqlState]) as Client;
}, [urqlClient, urqlState]);

return (
<Provider value={client}>
<Page urqlClient={client} {...(rest as T & IP)} />
<AppOrPage urqlClient={client} {...rest} />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for naming this AppOrPage – a much better description of what this can be!

</Provider>
);
};

// Set the displayName to indicate use of withUrqlClient.
const displayName = Page.displayName || Page.name || 'Component';
withUrql.displayName = `withUrqlClient(${displayName})`;
withUrql.displayName = `withUrqlClient(${getDisplayName(AppOrPage)})`;

withUrql.getInitialProps = async (ctx: NextPageContext | AppContext) => {
const { AppTree } = ctx;
withUrql.getInitialProps = async (appOrPageCtx: NextUrqlContext) => {
const { AppTree } = appOrPageCtx;

const appCtx = (ctx as AppContext).ctx;
const isApp = !!appCtx;
// 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 opts =
typeof clientConfig === 'function'
? clientConfig(isApp ? appCtx : (ctx as NextPageContext))
: clientConfig;
typeof clientConfig === 'function' ? clientConfig(ctx) : clientConfig;
const [urqlClient, ssrCache] = initUrqlClient(opts, mergeExchanges);

if (urqlClient) {
(ctx as NextUrqlContext).urqlClient = urqlClient;
}

// Run the wrapped component's getInitialProps function.
let pageProps = {} as IP;
if (Page.getInitialProps) {
pageProps = await Page.getInitialProps(ctx as NextUrqlContext);
let pageProps;
if (AppOrPage.getInitialProps) {
pageProps = await AppOrPage.getInitialProps(appOrPageCtx as any);
}

// Check the window object to determine whether or not we are on the server.
Expand All @@ -110,27 +79,18 @@ function withUrqlClient<T = any, IP = any>(
return pageProps;
}

// Run the prepass step on AppTree. This will run all urql queries on the server.
await ssrPrepass(
<AppTree
pageProps={{
...pageProps,
urqlClient,
}}
/>,
);
const props = { ...pageProps, urqlClient };
const appTreeProps = isApp ? props : { pageProps: props };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_app and Page components have different prop signatures, so we need to use the correct ones depending on what AppTree actually is.


// Extract the query data from urql's SSR cache.
const urqlState = ssrCache?.extractData();
// Run the prepass step on AppTree. This will run all urql queries on the server.
await ssrPrepass(<AppTree {...appTreeProps} />);

return {
...pageProps,
urqlState,
urqlState: ssrCache?.extractData(),
};
};

return withUrql;
};
}

export default withUrqlClient;