Giter VIP home page Giter VIP logo

Comments (3)

trompx avatar trompx commented on June 18, 2024 1

Thanks a lot for the fast answer Jack!

Keeping things separated is what I would have prefered, but I found the next js setup is way faster on my laptop, and I thought that, despite being still quite new, the future improvements of the next js team would mainly go to their api routes system.

That's almost as what I came up with, I had something like:

import { CONFIRM_PASSWORD } from "@graphql/user/queries"
import { initializeApollo } from '@services/apollo/apolloClient'

const Confirm = (props) => {
  if (props.userConfirmed) {
    return <div>User account has been confirmed</div>;
  } else {
    return <div>Something went wrong</div>;
  }
};

export async function getServerSideProps(context) {
  const apolloClient = initializeApollo()
  const { token } = context.query;

  const response = await apolloClient.mutate({
    mutation: CONFIRM_PASSWORD,
    variables: {
      token: token as string
    }
  })
  const userConfirmed = response.data.confirmUser

  // Or can redirect here directly to the login page

  return {
    props: {
      initialApolloState: apolloClient.cache.extract(),
      userConfirmed
    }
  }
}

export default Confirm

What I liked about the setup client side is that the auto generated code already took care of the apollo initialisation, thus not having to call initializeApollo on each component. But I guess there is no better way for now.

At first I had similar to the next js example:

function createIsomorphLink() {
  if (typeof window === 'undefined') {
    const { SchemaLink } = require('@apollo/client/link/schema')
    const { schema } = require('@services/apollo/schema');
    return new SchemaLink({ schema })
  } else {
    const { HttpLink } = require('@apollo/client/link/http')
    return new HttpLink({
      uri: GRAPHQL_URI,
      credentials: 'same-origin'
    })
  }
}

but had problem with the schema (getting entities not found after first request : due to HMR + typeorm problem) so went just like you did by creating a new link each time:

function createIsomorphLink() {
    const { HttpLink } = require('@apollo/client/link/http')
    return new HttpLink({
      uri: GRAPHQL_URI,
      credentials: 'same-origin'
    })
}

I decided to go with the session vs jwt token so will have to adapt the auth part.
I will definitly keep an eye on new best practices in the fullstack boilerplate repo ;)

from split.

JClackett avatar JClackett commented on June 18, 2024

Hi @trompx

Thanks for the kind words, glad that it helped you!

This project is slightly old now and we have updated the stack to use Next.js.

We've been quite busy with client projects so haven't had much time to do too much open source work. I would like to update our fullstack-boilerplate though with some new things that we have added, so keep an eye on that!

We still use Apollo Client with Next.js using getServerSideProps, quick example:

// pages/index.tsx
export const getServerSideProps: GetServerSideProps = async () => {
  const client = initializeApollo()
  await client.query({  query: MeQueryDocument,  errorPolicy: "ignore"  })
  return {
    props: {  initialApolloState: client.cache.extract() },
  }
}

then in your _app.tsx file:

// pages/_app.tsx
export default function MyApp(props: AppProps<{initialApolloState?: any}>) {
  const { Component, pageProps } = props
  const apolloClient = useApollo(pageProps.initialApolloState)
  return (
      <ApolloProvider client={apolloClient}>
         <Component {...pageProps} />
      </ApolloProvider>
  )
}

Related Apollo Client stuff including hook in _app

// lib/apolloClient.ts
import React from "react"
import { ApolloClient, InMemoryCache, NormalizedCacheObject, createHttpLink } from "@apollo/client"
import { setContext } from "@apollo/client/link/context"

import { API_URL, AUTH_TOKEN_NAME } from "lib/config"
import { parseCookies } from "lib/utils/helpers"

type Callback = () => string
type Options = {
  getToken: Callback
}

let apolloClient: ApolloClient<NormalizedCacheObject> | null = null

const httpLink = createHttpLink({ uri: API_URL })

function createApolloClient(options?: Options) {
  const authLink = setContext((_, { headers }) => {
    const token = options?.getToken()
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
      },
    }
  })

  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    ssrForceFetchDelay: 100,
    link: authLink.concat(httpLink),
    defaultOptions: {
      mutate: { errorPolicy: "all" },
      query: { errorPolicy: "all" },
    },
    cache: new InMemoryCache(),
  })
}

export function initializeApollo(initialState?: null | Record<string, any>, options?: Options) {
  const _apolloClient = apolloClient ?? createApolloClient(options)

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    const existingCache = _apolloClient.extract()
    // Merge existing cache with props passed from each page
    _apolloClient.cache.restore({ ...existingCache, ...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 = null) {
  const store = React.useMemo(
    () => initializeApollo(initialState, { getToken: () => parseCookies()[AUTH_TOKEN_NAME] }),
    [initialState],
  )
  return store
}

In the example above, the me query is run on the server (you can do as many queries as you want) and the cache is passed into page props, then a client side Apollo Client is instantiated using those props. Your components can now use this. So if there was a useMeQuery somewhere in the component tree, the value would have already been loaded so you won't get loading states!

For now, I think we will continue to use a separate next.js server + express Apollo server, mainly because it allows us to keep things separated in case we want to switch the frontend or use the api for non-next.js related things (app, other apis etc)

Hope this has been useful, let me know if you have more questions!

from split.

trompx avatar trompx commented on June 18, 2024

Hey @JClackett,

I have additional questions if you don't mind, concerning how you handle user authentification on client pages with redis session.

In my app, the graphql api authentification part is handled by an auth middleware (similar to your authChecker, except that I use @UseMiddleware(isAuth) instead). But for some pages, I wish to not display the page at all and redirect the user if he is not authenticated.

For now, in a page that you want to protect (like pages/Costs.tsx), you have:

import useAppContext from "../lib/hooks/useAppState"
...
  const { user } = useAppContext()
  if (!user.groupId) return <Redirect to="/" noThrow={true} />

which call the lib/hooks/useAppState.ts where the context is pulled from the application/context.tsx with:

export interface StateContext {
  user: MeQuery["me"]
  group: GetGroupQuery["group"]
}

export const StateContext = React.createContext<StateContext>({
  user: null,
  group: null,
})

export const StateProvider = StateContext.Provider

which itself exports a StateContext which I believe is populated with the user data pulled from the database inside the components/providers/StateProvider.tsx component.
It is then used in every pages/components needing to check if a user is auth, Is that how this works?

  1. I guess in next.js I should implement that AppProvider (including the StateProvider) in the _app.tsx file so every page have the user data in the context?
  2. I am wondering if you still have the same approach of handling protected content on the client in next js, or maybe you use an HOC now instead?
  3. And does it mean that each time that a page, or a component, using useAppContext is called, the MeQuery is run? So if I have a complex page with multiple components checking with const { user } = useAppContext(), does it mean that the page would query as many times the redis database as the useAppContext function is called or there is some sort of cache mechanism?

from split.

Related Issues (2)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.