Skip to content

Instantly share code, notes, and snippets.

@TrillCyborg
Created November 14, 2019 21:08
Show Gist options
  • Save TrillCyborg/7ce2ba18e89176cbff02f1b86a262ca1 to your computer and use it in GitHub Desktop.
Save TrillCyborg/7ce2ba18e89176cbff02f1b86a262ca1 to your computer and use it in GitHub Desktop.

Revisions

  1. TrillCyborg created this gist Nov 14, 2019.
    67 changes: 67 additions & 0 deletions apollo.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,67 @@
    import fetch from 'isomorphic-unfetch'
    import { ApolloClient } from 'apollo-client'
    import { ApolloLink } from 'apollo-link'
    import { createHttpLink } from 'apollo-link-http'
    import { InMemoryCache, NormalizedCacheObject } from 'apollo-cache-inmemory'
    import { AccountsGraphQLClient } from '@accounts/graphql-client'
    import { AccountsClientPassword } from '@accounts/client-password'
    import { AccountsClient } from '@accounts/client'
    import { accountsLink } from '@accounts/apollo-link'
    import { tokenStorage } from './token-storage'

    let apolloClient: {
    client: ApolloClient<NormalizedCacheObject>
    accountsGraphQL: AccountsGraphQLClient
    accountsPassword: AccountsClientPassword
    } = null

    function create(opts?: { ctx?: any; initialState?: any }) {
    // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
    const isBrowser = typeof window !== 'undefined'
    const cache = new InMemoryCache().restore(opts.initialState || {})
    const httpLink = createHttpLink({
    uri: process.env.GRAPHQL_URL, // Server URL (must be absolute)
    credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
    // Use fetch() polyfill on the server
    fetch: !isBrowser && fetch,
    })
    const graphQLApolloClient = new ApolloClient({
    link: ApolloLink.from([httpLink]),
    cache,
    })
    const accountsGraphQL = new AccountsGraphQLClient({
    graphQLClient: graphQLApolloClient,
    })
    const accountsClient = new AccountsClient(
    {
    tokenStorage: tokenStorage(opts.ctx),
    },
    accountsGraphQL
    )
    const accountsPassword = new AccountsClientPassword(accountsClient)
    // regular apollo client
    const authLink = accountsLink(() => accountsClient)
    const client = new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once)
    link: ApolloLink.from([authLink, httpLink]),
    cache,
    })

    return { client, accountsGraphQL, accountsPassword, accountsClient }
    }

    export function initApollo(opts?: { ctx?: any; 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 create(opts)
    }

    // Reuse client on the client-side
    if (!apolloClient) {
    apolloClient = create(opts)
    }

    return apolloClient
    }
    19 changes: 19 additions & 0 deletions token-storage.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    import cookies from 'next-cookies'
    import Cookies from 'js-cookie'

    export const tokenStorage = (ctx: any) => ({
    setItem: async (key: string, value: string) => {
    Cookies.set(key, value)
    },
    getItem: async (key: string) => {
    const allCookies = cookies(ctx)
    const item = allCookies[escape(key)] || allCookies[key]
    return item
    },
    removeItem: async (key: string) => {
    Cookies.remove(key)
    },
    getItemClientSync: (key: string) => {
    return Cookies.get(key)
    },
    })
    126 changes: 126 additions & 0 deletions with-apollo.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,126 @@
    import React, { createContext, useMemo } from 'react'
    import Head from 'next/head'
    import { ApolloClient } from 'apollo-client'
    import { ApolloProvider } from '@apollo/react-hooks'
    import { NormalizedCacheObject } from 'apollo-cache-inmemory'
    import { AccountsGraphQLClient } from '@accounts/graphql-client'
    import { AccountsClientPassword } from '@accounts/client-password'
    import { AccountsClient } from '@accounts/client'
    import { initApollo } from './apollo'

    export const ApolloContext = createContext<{
    client: ApolloClient<NormalizedCacheObject>
    accountsGraphQL: AccountsGraphQLClient
    accountsPassword: AccountsClientPassword
    accountsClient: AccountsClient
    }>({
    client: null,
    accountsGraphQL: null,
    accountsPassword: null,
    accountsClient: null,
    })

    /**
    * Creates and provides the apolloContext
    * to a next.js PageTree. Use it by wrapping
    * your PageComponent via HOC pattern.
    * @param {Function|Class} PageComponent
    * @param {Object} [config]
    * @param {Boolean} [config.ssr=true]
    */
    export function withApollo(PageComponent, { ssr = true } = {}) {
    const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
    const { client, accountsGraphQL, accountsPassword, accountsClient } = useMemo(
    () => apolloClient || initApollo({ initialState: apolloState }),
    []
    )

    return (
    <ApolloProvider client={client}>
    <ApolloContext.Provider
    value={{ accountsGraphQL, accountsPassword, accountsClient, client }}
    >
    <PageComponent
    {...pageProps}
    accountsGraphQL={accountsGraphQL as AccountsGraphQLClient}
    accountsPassword={accountsPassword as AccountsClientPassword}
    accountsClient={accountsClient as AccountsClient}
    />
    </ApolloContext.Provider>
    </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 = initApollo({ ctx }))

    // 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.client.cache.extract()

    return {
    ...pageProps,
    apolloState,
    }
    }
    }

    return WithApollo
    }

    export default withApollo