From 43271821f2f27ddda7897b5c3090630b0e409c99 Mon Sep 17 00:00:00 2001 From: Luis Alvarez D Date: Tue, 9 Jun 2020 23:14:53 -0500 Subject: [PATCH] [Examples] Move with-typescript-graphql to SSG (#13854) Related to https://github.com/vercel/next.js/issues/11014 --- examples/with-stitches/css/index.js | 2 +- examples/with-typescript-graphql/.gitignore | 35 ++- examples/with-typescript-graphql/README.md | 18 +- .../with-typescript-graphql/lib/apollo.ts | 59 +++++ .../with-typescript-graphql/lib/resolvers.ts | 2 +- .../with-typescript-graphql/lib/schema.ts | 4 +- .../lib/with-apollo.tsx | 203 ------------------ examples/with-typescript-graphql/package.json | 15 +- .../with-typescript-graphql/pages/_app.tsx | 13 ++ .../with-typescript-graphql/pages/about.tsx | 4 +- .../pages/api/graphql.ts | 8 +- .../with-typescript-graphql/pages/index.tsx | 42 ++-- 12 files changed, 152 insertions(+), 253 deletions(-) create mode 100644 examples/with-typescript-graphql/lib/apollo.ts delete mode 100644 examples/with-typescript-graphql/lib/with-apollo.tsx create mode 100644 examples/with-typescript-graphql/pages/_app.tsx diff --git a/examples/with-stitches/css/index.js b/examples/with-stitches/css/index.js index 7357b42f75f0..810d0e4bfa19 100644 --- a/examples/with-stitches/css/index.js +++ b/examples/with-stitches/css/index.js @@ -17,7 +17,7 @@ const context = React.createContext(null) /* With Typescript: - const Provider: React.FC<{ css: TCss }> = ({ css, children }) => { + const Provider = ({ css, children }: { css: TCss, children?: React.ReactNode }) => { return {children} } */ diff --git a/examples/with-typescript-graphql/.gitignore b/examples/with-typescript-graphql/.gitignore index d5a507678d9e..7101814d1389 100644 --- a/examples/with-typescript-graphql/.gitignore +++ b/examples/with-typescript-graphql/.gitignore @@ -1,4 +1,35 @@ -.next -node_modules +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Graphql *.graphql.d.ts *.graphqls.d.ts diff --git a/examples/with-typescript-graphql/README.md b/examples/with-typescript-graphql/README.md index 6f686cfa75c5..3c7f84334f5b 100644 --- a/examples/with-typescript-graphql/README.md +++ b/examples/with-typescript-graphql/README.md @@ -2,18 +2,21 @@ One of the strengths of GraphQL is [enforcing data types on runtime](https://graphql.github.io/graphql-spec/June2018/#sec-Value-Completion). Further, TypeScript and [GraphQL Code Generator](https://graphql-code-generator.com/) (graphql-codegen) make it safer by typing data statically, so you can write truly type-protected code with rich IDE assists. -This template extends [Apollo Server and Client Example](https://github.com/vercel/next.js/tree/canary/examples/api-routes-apollo-server-and-client#readme) by rewriting in TypeScript and integrating [graphql-let](https://github.com/piglovesyou/graphql-let#readme), which runs [TypeScript React Apollo](https://graphql-code-generator.com/docs/plugins/typescript-react-apollo) in [graphql-codegen](https://github.com/dotansimha/graphql-code-generator#readme) under the hood. It enhances the typed GraphQL use as below. +This template extends [Apollo Server and Client Example](https://github.com/vercel/next.js/tree/canary/examples/api-routes-apollo-server-and-client#readme) by rewriting in TypeScript and integrating [graphql-let](https://github.com/piglovesyou/graphql-let#readme), which runs [TypeScript React Apollo](https://graphql-code-generator.com/docs/plugins/typescript-react-apollo) in [graphql-codegen](https://github.com/dotansimha/graphql-code-generator#readme) under the hood. It enhances the typed GraphQL use as below: -```typescript jsx +```tsx import { useNewsQuery } from './news.graphql' -const News: React.FC = () => { +const News = () => { // Typed already️⚡️ const { data: { news } } = useNewsQuery() - if (news)
{news.map(...)}
+ + return
{news.map(...)}
} ``` +By default `**/*.graphqls` is recognized as GraphQL schema and `**/*.graphql` as GraphQL documents. If you prefer the other extensions, make sure the settings of the webpack loader in `next.config.js` and `.graphql-let.yml` are consistent. + ## Deploy your own Deploy the example using [Vercel](https://vercel.com): @@ -52,10 +55,3 @@ yarn dev ``` Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). - -## Notes - -By default `**/*.graphqls` is recognized as GraphQL schema and `**/*.graphql` as GraphQL documents. If you prefer the other extensions, make sure the settings of the webpack loader in `next.config.js` and `.graphql-let.yml` are consistent. - -Note: Do not be alarmed that you see two renders being executed. Apollo recursively traverses the React render tree looking for Apollo query components. When it has done that, it fetches all these queries and then passes the result to a cache. This cache is then used to render the data on the server side (another React render). -https://www.apollographql.com/docs/react/api/react-ssr/#getdatafromtree diff --git a/examples/with-typescript-graphql/lib/apollo.ts b/examples/with-typescript-graphql/lib/apollo.ts new file mode 100644 index 000000000000..4c49c80edfba --- /dev/null +++ b/examples/with-typescript-graphql/lib/apollo.ts @@ -0,0 +1,59 @@ +import { IncomingMessage, ServerResponse } from 'http' +import { useMemo } from 'react' +import { ApolloClient } from 'apollo-client' +import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory' + +let apolloClient: ApolloClient | undefined + +export type ResolverContext = { + req?: IncomingMessage + res?: ServerResponse +} + +function createIsomorphLink(context: ResolverContext = {}) { + if (typeof window === 'undefined') { + const { SchemaLink } = require('apollo-link-schema') + const { schema } = require('./schema') + return new SchemaLink({ schema, context }) + } else { + const { HttpLink } = require('apollo-link-http') + return new HttpLink({ + uri: '/api/graphql', + credentials: 'same-origin', + }) + } +} + +function createApolloClient(context?: ResolverContext) { + return new ApolloClient({ + ssrMode: typeof window === 'undefined', + link: createIsomorphLink(context), + cache: new InMemoryCache(), + }) +} + +export function initializeApollo( + initialState: any = null, + // Pages with Next.js data fetching methods, like `getStaticProps`, can send + // a custom context which will be used by `SchemaLink` to server render pages + context?: ResolverContext +) { + const _apolloClient = apolloClient ?? createApolloClient(context) + + // If your page has Next.js data fetching methods that use Apollo Client, the initial state + // get hydrated here + if (initialState) { + _apolloClient.cache.restore(initialState) + } + // For SSG and SSR always create a new Apollo Client + if (typeof window === 'undefined') return _apolloClient + // Create the Apollo Client once in the client + if (!apolloClient) apolloClient = _apolloClient + + return _apolloClient +} + +export function useApollo(initialState: any) { + const store = useMemo(() => initializeApollo(initialState), [initialState]) + return store +} diff --git a/examples/with-typescript-graphql/lib/resolvers.ts b/examples/with-typescript-graphql/lib/resolvers.ts index 840403857c56..f7989648bdf1 100644 --- a/examples/with-typescript-graphql/lib/resolvers.ts +++ b/examples/with-typescript-graphql/lib/resolvers.ts @@ -1,5 +1,5 @@ import { QueryResolvers } from './type-defs.graphqls' -import { ResolverContext } from './with-apollo' +import { ResolverContext } from './apollo' const Query: Required> = { viewer(_parent, _args, _context, _info) { diff --git a/examples/with-typescript-graphql/lib/schema.ts b/examples/with-typescript-graphql/lib/schema.ts index ed11df67f14b..892911d18a6c 100644 --- a/examples/with-typescript-graphql/lib/schema.ts +++ b/examples/with-typescript-graphql/lib/schema.ts @@ -2,9 +2,7 @@ import { makeExecutableSchema } from 'graphql-tools' import typeDefs from './type-defs.graphqls' import resolvers from './resolvers' -const schema = makeExecutableSchema({ +export const schema = makeExecutableSchema({ typeDefs, resolvers, }) - -export default schema diff --git a/examples/with-typescript-graphql/lib/with-apollo.tsx b/examples/with-typescript-graphql/lib/with-apollo.tsx deleted file mode 100644 index 89d3f315ba15..000000000000 --- a/examples/with-typescript-graphql/lib/with-apollo.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { IncomingMessage, ServerResponse } from 'http' -import { NextPage, NextPageContext } from 'next' -import { ContextFunction } from 'apollo-server-core' -import React from 'react' -import Head from 'next/head' -import { ApolloProvider } from '@apollo/react-hooks' -import { ApolloClient } from 'apollo-client' -import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory' - -type TApolloClient = ApolloClient - -type InitialProps = { - apolloClient: TApolloClient - apolloState: any -} & Record - -type WithApolloPageContext = { - apolloClient: TApolloClient -} & NextPageContext - -export type ResolverContext = { req: IncomingMessage; res: ServerResponse } - -let globalApolloClient: TApolloClient - -export const createResolverContext: ContextFunction< - { req: IncomingMessage; res: ServerResponse }, - ResolverContext -> = async ({ req, res }) => { - // If you want to pass additional data to resolvers as context - // such as session data, you can do it here. For example: - // - // const user = await resolveUser(req.header.cookie) - // return { req, res, user } - // - return { req, res } -} - -/** - * Creates and provides the apolloContext - * to a next.js PageTree. Use it by wrapping - * your PageComponent via HOC pattern. - * By passing `{ssr: false}`, it could be statically optimized - * instead of being exported as a serverless function. - */ -export default function withApollo( - PageComponent: NextPage, - { ssr = true } = {} -) { - const WithApollo = ({ - apolloClient, - apolloState, - ...pageProps - }: InitialProps) => { - const client = apolloClient || initApolloClient(apolloState) - return ( - - - - ) - } - - // Set the correct displayName in development - if (process.env.NODE_ENV !== 'production') { - const displayName = - PageComponent.displayName || PageComponent.name || 'Component' - - if (displayName === 'App') { - console.warn('This withApollo HOC only works with PageComponents.') - } - - WithApollo.displayName = `withApollo(${displayName})` - } - - if (ssr || PageComponent.getInitialProps) { - WithApollo.getInitialProps = async (ctx: WithApolloPageContext) => { - // Resolver context here is only set on server. For client-side, - // "/api/graphql" route creates and pass it to resolver functions. - let resolverContext: ResolverContext | undefined - // Keep the "isServer" check inline, so webpack removes the block - // for client-side bundle. - if (typeof window === 'undefined') { - resolverContext = await createResolverContext({ - req: ctx.req!, - res: ctx.res!, - }) - } - - // Initialize ApolloClient, add it to the ctx object so - // we can use it in `PageComponent.getInitialProp`. - const apolloClient = (ctx.apolloClient = initApolloClient( - undefined, - resolverContext - )) - - // Run wrapped getInitialProps methods - let pageProps = {} - if (PageComponent.getInitialProps) { - pageProps = await PageComponent.getInitialProps(ctx) - } - - // Only on the server: - if (typeof window === 'undefined') { - // When redirecting, the response is finished. - // No point in continuing to render - if (ctx.res && ctx.res.finished) { - return pageProps - } - - // Only if ssr is enabled - if (ssr) { - try { - const { AppTree } = ctx - // Run all GraphQL queries - const { getDataFromTree } = await import('@apollo/react-ssr') - await getDataFromTree( - - ) - } catch (error) { - // Prevent Apollo Client GraphQL errors from crashing SSR. - // Handle them in components via the data.error prop: - // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error - console.error('Error while running `getDataFromTree`', error) - } - - // getDataFromTree does not call componentWillUnmount - // head side effect therefore need to be cleared manually - Head.rewind() - } - } - - // Extract query data from the Apollo store - const apolloState = apolloClient.cache.extract() - - return { - ...pageProps, - apolloState, - } - } - } - - return WithApollo -} - -/** - * Always creates a new apollo client on the server - * Creates or reuses apollo client in the browser. - */ -function initApolloClient( - initialState?: any, - resolverContext?: ResolverContext -) { - // Make sure to create a new client for every server-side request so that data - // isn't shared between connections (which would be bad) - if (typeof window === 'undefined') { - return createApolloClient(initialState, resolverContext) - } - - // Reuse client on the client-side - if (!globalApolloClient) { - globalApolloClient = createApolloClient(initialState) - } - - return globalApolloClient -} - -/** - * Creates and configures the ApolloClient - */ -function createApolloClient( - initialState = {}, - resolverContext?: ResolverContext -) { - const ssrMode = typeof window === 'undefined' - const cache = new InMemoryCache().restore(initialState) - - // Check out https://github.com/vercel/next.js/pull/4611 if you want to use the AWSAppSyncClient - return new ApolloClient({ - ssrMode, - link: createIsomorphLink(resolverContext), - cache, - }) -} - -function createIsomorphLink(resolverContext?: ResolverContext) { - if (typeof window === 'undefined') { - const { SchemaLink } = require('apollo-link-schema') - const schema = require('./schema').default - - // "resolverContext" is passed only before calling "getDataFromTree". - return new SchemaLink({ schema, context: resolverContext }) - } else { - const { HttpLink } = require('apollo-link-http') - return new HttpLink({ - uri: '/api/graphql', - credentials: 'same-origin', - }) - } -} diff --git a/examples/with-typescript-graphql/package.json b/examples/with-typescript-graphql/package.json index 6e2a7c440eed..2e87e097a8eb 100644 --- a/examples/with-typescript-graphql/package.json +++ b/examples/with-typescript-graphql/package.json @@ -13,14 +13,13 @@ "@apollo/react-common": "3.1.4", "@apollo/react-components": "^3.1.5", "@apollo/react-hooks": "3.1.5", - "@apollo/react-ssr": "3.1.5", - "apollo-cache": "1.3.4", - "apollo-cache-inmemory": "1.6.5", - "apollo-client": "2.6.8", + "apollo-cache": "^1.3.5", + "apollo-cache-inmemory": "^1.6.6", + "apollo-client": "^2.6.10", "apollo-link": "1.2.14", "apollo-link-http": "1.5.17", "apollo-link-schema": "1.2.5", - "apollo-server-micro": "2.12.0", + "apollo-server-micro": "^2.14.2", "apollo-utilities": "^1.3.3", "graphql": "^14.6.0", "graphql-tag": "^2.10.3", @@ -29,12 +28,12 @@ "react-dom": "^16.13.1" }, "devDependencies": { - "@graphql-codegen/cli": "1.13.3", - "@graphql-codegen/plugin-helpers": "1.13.3", + "@graphql-codegen/cli": "^1.15.1", + "@graphql-codegen/plugin-helpers": "^1.15.1", "@graphql-codegen/typescript": "^1.13.3", "@graphql-codegen/typescript-operations": "^1.13.3", "@graphql-codegen/typescript-react-apollo": "^1.13.3", - "@graphql-codegen/typescript-resolvers": "1.13.3", + "@graphql-codegen/typescript-resolvers": "^1.15.1", "@types/react": "^16.9.34", "@types/react-dom": "^16.9.7", "graphql-let": "0.x", diff --git a/examples/with-typescript-graphql/pages/_app.tsx b/examples/with-typescript-graphql/pages/_app.tsx new file mode 100644 index 000000000000..73628f4994e5 --- /dev/null +++ b/examples/with-typescript-graphql/pages/_app.tsx @@ -0,0 +1,13 @@ +import { AppProps } from 'next/app' +import { ApolloProvider } from '@apollo/react-hooks' +import { useApollo } from '../lib/apollo' + +export default function App({ Component, pageProps }: AppProps) { + const apolloClient = useApollo(pageProps.initialApolloState) + + return ( + + + + ) +} diff --git a/examples/with-typescript-graphql/pages/about.tsx b/examples/with-typescript-graphql/pages/about.tsx index 4d21f1676c4c..36ce20fa93b7 100644 --- a/examples/with-typescript-graphql/pages/about.tsx +++ b/examples/with-typescript-graphql/pages/about.tsx @@ -3,9 +3,9 @@ import Link from 'next/link' export default function About() { return (
- This is a static page goto{' '} + Welcome to the about page. Go to the{' '} - dynamic + Home {' '} page.
diff --git a/examples/with-typescript-graphql/pages/api/graphql.ts b/examples/with-typescript-graphql/pages/api/graphql.ts index 2240c4becdf1..2018e8703411 100644 --- a/examples/with-typescript-graphql/pages/api/graphql.ts +++ b/examples/with-typescript-graphql/pages/api/graphql.ts @@ -1,11 +1,7 @@ import { ApolloServer } from 'apollo-server-micro' -import schema from '../../lib/schema' -import { createResolverContext } from '../../lib/with-apollo' +import { schema } from '../../lib/schema' -const apolloServer = new ApolloServer({ - schema, - context: createResolverContext, -}) +const apolloServer = new ApolloServer({ schema }) export const config = { api: { diff --git a/examples/with-typescript-graphql/pages/index.tsx b/examples/with-typescript-graphql/pages/index.tsx index de65953a143b..488c08065711 100644 --- a/examples/with-typescript-graphql/pages/index.tsx +++ b/examples/with-typescript-graphql/pages/index.tsx @@ -1,24 +1,34 @@ -import withApollo from '../lib/with-apollo' import Link from 'next/link' -import { useViewerQuery } from '../lib/viewer.graphql' +import { useViewerQuery, ViewerDocument } from '../lib/viewer.graphql' +import { initializeApollo } from '../lib/apollo' const Index = () => { const { data } = useViewerQuery() + const { viewer } = data! - if (data) { - const { viewer } = data - return ( -
- You're signed in as {viewer.name} and you're {viewer.status} goto{' '} - - static - {' '} - page. -
- ) - } + return ( +
+ You're signed in as {viewer.name} and you're {viewer.status} go to the{' '} + + about + {' '} + page. +
+ ) +} + +export async function getStaticProps() { + const apolloClient = initializeApollo() - return
...
+ await apolloClient.query({ + query: ViewerDocument, + }) + + return { + props: { + initialApolloState: apolloClient.cache.extract(), + }, + } } -export default withApollo(Index) +export default Index