Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposing a new example "with-typescript-graphql" #9803

Merged
merged 8 commits into from
Jan 8, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/with-typescript-graphql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__generated__
7 changes: 7 additions & 0 deletions examples/with-typescript-graphql/.graphql-let.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
generateDir: __generated__
schema: schema/**/*.graphqls
documents: pages/**/*.graphql
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
63 changes: 63 additions & 0 deletions examples/with-typescript-graphql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# GraphQL and TypeScript Example

## Deploy your own

Deploy the example using [ZEIT Now](https://zeit.co/now):

[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-typescript-graphql)

## How to use

### Using `create-next-app`

Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:

```bash
npx create-next-app --example with-typescript-graphql with-typescript-graphql-app
# or
yarn create next-app --example with-typescript-graphql with-typescript-graphql-app
```

### Download manually

Download the example:

```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-typescript-graphql
cd with-typescript-graphql
```

Install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):

```bash
now
```

## The idea behind the example

One of the strengths of GraphQL is enforcing data types on runtime. Further, TypeScript and [GraphQL Code Generator](https://graphql-code-generator.com/) (grapql-codegen) make it safer to type 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/zeit/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 graphql-codegen under the hood and enhances the use of typed GraphQL operation, like below.

```typescript jsx
import { useNewsQuery } from './news.grpahql'

const News: React.FC<{}> = (props) => {
// Typed already️⚡️
const { data: { news } } = useNewsQuery()
if (news) <div>{news.map(...)}</div>
}
```

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
7 changes: 7 additions & 0 deletions examples/with-typescript-graphql/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

declare module '*.graphqls' {
import { DocumentNode } from 'graphql'
export default typeof DocumentNode
}
3 changes: 3 additions & 0 deletions examples/with-typescript-graphql/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const withTypeScriptGraphQL = require('./with-typescript-graphql')
lfades marked this conversation as resolved.
Show resolved Hide resolved

module.exports = withTypeScriptGraphQL()
37 changes: 37 additions & 0 deletions examples/with-typescript-graphql/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "with-typescript-graphql",
"version": "0.1.0",
"author": "",
"license": "ISC",
"dependencies": {
"@apollo/react-common": "3.1.3",
"@apollo/react-components": "^3.1.3",
"@apollo/react-hooks": "3.1.3",
"@apollo/react-ssr": "3.1.3",
"apollo-cache-inmemory": "1.6.5",
"apollo-client": "2.6.8",
"apollo-link-http": "1.5.16",
"apollo-link-schema": "1.2.4",
"apollo-server-micro": "2.9.14",
"apollo-utilities": "^1.3.3",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"next": "latest",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
"devDependencies": {
"@graphql-codegen/typescript": "^1.9.1",
"@graphql-codegen/typescript-operations": "^1.9.1",
"@graphql-codegen/typescript-react-apollo": "^1.9.1",
"@types/react": "^16.9.17",
"graphql-let": "0.3.0",
"typescript": "^3.7.4"
},
"scripts": {
"codegen": "graphql-let",
"dev": "yarn codegen && next",
"build": "yarn codegen && next build",
"start": "next start"
}
}
11 changes: 11 additions & 0 deletions examples/with-typescript-graphql/pages/about.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Link from 'next/link'

export default () => (
<div>
This is a static page goto{' '}
<Link href="/">
<a>dynamic</a>
</Link>{' '}
page.
</div>
)
12 changes: 12 additions & 0 deletions examples/with-typescript-graphql/pages/api/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApolloServer } from 'apollo-server-micro'
import schema from '../../schema'

const apolloServer = new ApolloServer({ schema })

export const config = {
api: {
bodyParser: false,
},
}

export default apolloServer.createHandler({ path: '/api/graphql' })
24 changes: 24 additions & 0 deletions examples/with-typescript-graphql/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import withGraphQL from '../schema/with-graphql'
import Link from 'next/link'
import { useViewerQuery } from './viewer.graphql'

const Index = () => {
const { data } = useViewerQuery()

if (data) {
const { viewer } = data
return (
<div>
You're signed in as {viewer.name} and you're {viewer.status} goto{' '}
<Link href="/about">
<a>static</a>
</Link>{' '}
page.
</div>
)
}

return <div>...</div>
}

export default withGraphQL(Index)
7 changes: 7 additions & 0 deletions examples/with-typescript-graphql/pages/viewer.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query Viewer {
viewer {
id
name
status
}
}
10 changes: 10 additions & 0 deletions examples/with-typescript-graphql/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { makeExecutableSchema } from 'graphql-tools'
import typeDefs from './type-defs.graphqls'
import resolvers from './resolvers'

const schema = makeExecutableSchema({
typeDefs,
resolvers,
})

export default schema
9 changes: 9 additions & 0 deletions examples/with-typescript-graphql/schema/resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const resolvers = {
Query: {
viewer(_parent, _args, _context, _info) {
return { id: 1, name: 'John Smith', status: 'cached' }
},
},
}

export default resolvers
9 changes: 9 additions & 0 deletions examples/with-typescript-graphql/schema/type-defs.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type User {
lfades marked this conversation as resolved.
Show resolved Hide resolved
id: ID!
name: String!
status: String!
}

type Query {
viewer: User
}
149 changes: 149 additions & 0 deletions examples/with-typescript-graphql/schema/with-graphql.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { NextPage } from 'next'
import React from 'react'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'

let apolloClient = null

/**
* Creates and provides the apolloContext
* to a next.js PageTree. Use it by wrapping
* your PageComponent via HOC pattern.
*/
export default function withGraphQL(
PageComponent: NextPage,
{ ssr = true } = {}
) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState)
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
)
}

// 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 => {
const { AppTree } = ctx

// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient())

// 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 {
// Run all GraphQL queries
const { getDataFromTree } = await import('@apollo/react-ssr')
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient,
}}
/>
)
} 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.
* @param {Object} initialState
*/
function initApolloClient(initialState?: any) {
// 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)
}

// Reuse client on the client-side
if (!apolloClient) {
apolloClient = createApolloClient(initialState)
}

return apolloClient
}

/**
* Creates and configures the ApolloClient
* @param {Object} [initialState={}]
*/
function createApolloClient(initialState = {}) {
const ssrMode = typeof window === 'undefined'
const cache = new InMemoryCache().restore(initialState)

// Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
return new ApolloClient({
ssrMode,
link: createIsomorphLink(),
cache,
})
}

function createIsomorphLink() {
if (typeof window === 'undefined') {
const { SchemaLink } = require('apollo-link-schema')
const schema = require('./index').default
return new SchemaLink({ schema })
} else {
const { HttpLink } = require('apollo-link-http')
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
}
}
19 changes: 19 additions & 0 deletions examples/with-typescript-graphql/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": false,
lfades marked this conversation as resolved.
Show resolved Hide resolved
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}