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

Example with getSession() in _app.js? #345

Closed
1 of 5 tasks
theogravity opened this issue Jun 27, 2020 · 17 comments · Fixed by #351
Closed
1 of 5 tasks

Example with getSession() in _app.js? #345

theogravity opened this issue Jun 27, 2020 · 17 comments · Fixed by #351
Labels
question Ask how to do something or how something works

Comments

@theogravity
Copy link
Contributor

theogravity commented Jun 27, 2020

Your question

All the examples I've seen for getSession() is for a specific page, but I'd like to pass the session prop at the layout level, via _app.js.

What is the proper way of using it in _app.js for both client/server side application?

What are you trying to do

Apply the session prop to all page props without duplicating logic by setting it through _app.js.

// _app.tsx
// some psuedo-code
import { Provider } from 'react-redux'
import { getSession, Provider as AuthProvider } from 'next-auth/client'

const WrappedApp: FC<CustomProps> & StaticComponents = (props) => {
  const { Component, pageProps, session } = props

  return (
    <Provider store={store}>
      <AuthProvider session={session} options={{ site: 'http://localhost:3000' }}>
            <HeaderNav {...props} />
            <Component {...pageProps} />
      </AuthProvider>
    </Provider>
  )
}

// https://github.com/isaachinman/next-i18next/issues/615
WrappedApp.getInitialProps = async (context) => {
  const appProps = await App.getInitialProps(context)
  const session = await getSession(context)
 
  // ----------> returns null
  console.log(session)

  return {
    ...appProps,
    session
  }
}

export default WrappedApp

With the example, I also get despite having the proper site value:

[next-auth][error][CLIENT_FETCH_ERROR] [
  '/api/auth/session',
  TypeError: Only absolute URLs are supported

Documentation feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.

  • Found the documentation helpful
  • Found documentation but was incomplete
  • Could not find relevant documentation
  • Found the example project helpful
  • Did not find the example project helpful
@theogravity theogravity added the question Ask how to do something or how something works label Jun 27, 2020
@iaincollins
Copy link
Member

iaincollins commented Jun 27, 2020

I agree, I think this a thing it would be helpful to have a tutorial for. This is tricky as the Provider isn't initialised yet so doesn't help if you want to call getSession() from getInitialProps() in _app.js.

There is an undocumented work around, where you can export and call setOptions() directly from next-auth/client, as in the example below, which should solve this problem.

import { Provider } from 'react-redux'
import { setOptions, getSession, Provider as AuthProvider } from 'next-auth/client'
setOptions({ site: 'http://localhost:3000 })

const WrappedApp: FC<CustomProps> & StaticComponents = (props) => {
  const { Component, pageProps, session } = props

  return (
    <Provider store={store}>
      <AuthProvider session={session} options={{ site: 'http://localhost:3000' }}>
            <HeaderNav {...props} />
            <Component {...pageProps} />
      </AuthProvider>
    </Provider>
  )
}

WrappedApp.getInitialProps = async (context) => {
  const appProps = await App.getInitialProps(context)
  const session = await getSession(context)

  return {
    ...appProps,
    session
  }
}

export default WrappedApp

Note: You can pass a regular environment variable (e.g. process.env.SITE) and pass it as the value for the site option.

@theogravity
Copy link
Contributor Author

theogravity commented Jun 28, 2020

Thanks for the response. I no longer get the fetch error, but the session value still comes out as null. I've verified that I do have an active session as useSession() will work on the client side.

Another question - how do folks generally implement getSession() in a reusable way? Is calling the method on every page the thing to do in next when you have common init/fetch logic across pages?

@theogravity
Copy link
Contributor Author

theogravity commented Jun 28, 2020

@iaincollins I think I see where the problem is, and I believe it's a bug in the client code under this particular situation.

I put some log statements in dist/client/index.js:

var getSession = function () {
  var _ref = _asyncToGenerator(function* () {
    var {
      req
    } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

    var baseUrl = _baseUrl();

    var options = req ? {
      headers: {
        cookie: req.headers.cookie
      }
    } : {};

    console.log(arguments)
    console.log('req', req)
    console.log('opts', options)
    console.log(baseUrl, "/session")

The output looks like this:

[Arguments] {
  '0': {
    AppTree: [Function: AppTree],
    Component: [Function: IndexPage] { getNavbarContent: [Function] },
    router: ServerRouter {
      route: '/',
      pathname: '/',
      query: {},
      asPath: '/',
      basePath: '',
      events: undefined,
      isFallback: false
    },
    ctx: {
      err: undefined,
      req: [IncomingMessage],
      res: [ServerResponse],
      pathname: '/',
      query: {},
      asPath: '/',
      AppTree: [Function: AppTree]
    }
  }
}
req undefined
opts {}
http://localhost:3000/api/auth /session

req is undefined because it's looking for it directly under arguments[0] when it's actually under arguments[0].ctx.req in this case.

If I do this, the flow works, but it feels quite hacky:

WrappedApp.getInitialProps = async (context) => {
  context['req'] = context.ctx.req

Not sure what the appropriate fix would be for getSession() since I'm pretty new to next.

@iaincollins
Copy link
Member

Another question - how do folks generally implement getSession() in a reusable way? Is calling the method on every page the thing to do in next when you have common init/fetch logic across pages?

The best approach is to use the useSession() hook, as it will share the session object between components (and even between page navigations) and avoids constantly calling the server.

If you need server side rendering then yes, you need to call getSession() on every page view.

You can do that by creating an 'AuthPage' class and extending it (or a HOC and using that) to apply it only to pages that require authentication, or add it to _app.js as you've done here, if all pages on your site require authentication with server side rendering.

We plan to do some tutorial for folks in future to help with this.

Thanks for you PR will leave a note there!

@theogravity
Copy link
Contributor Author

I went with this route because useSession would have this brief flash where the page renders in a logout state then immediately flashes the layout to the login one, which was pretty jarring.

@iaincollins
Copy link
Member

The example project doesn't have that problem and was created to show how to avoid that, including on sites that also need to support server side rendering on some pages.

If you go this route, do aware your website will be slower to access as a result and Next.js will generate a warning if you do this because it prevents automatic static optimization of your site.

The performance impact isn't usually noticeable running locally as everything runs very quickly, but can be significant when the site is deployed to Vercel / AWS Lambda.

Related: I've added some examples of alternative approaches to securing routes in #347 that contain approaches that are somewhat relevant.

@theogravity
Copy link
Contributor Author

Thank you so much for your time and providing guidance. I'll check out your updated examples.

@iaincollins
Copy link
Member

I'm happy to leave this issue open until we have a more satisfying answer to your question. :-)

@erhankaradeniz
Copy link

I know this issue is closed, but would it be possible to pass crsfToken on _app too? (i'm asking because I have a few pages where I check if a user is logged in or not after a certain action, e.g. click on a button).

For this I'm fetching the session and csrfToken on pageload or on the server. It would be nice if we could pass those down, so we don't need to check on those pages.. (this means I'd be able to have those pages be SSG instead of SSR).

@erhankaradeniz
Copy link

As a side note, I'm fetching those since after the click. I'm showing a login modal, where I need to pass the csrf for email login.

iaincollins added a commit that referenced this issue Jul 8, 2020
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
iaincollins added a commit that referenced this issue Jul 10, 2020
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
iaincollins added a commit that referenced this issue Jul 24, 2020
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
iaincollins added a commit that referenced this issue Jul 27, 2020
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
iaincollins added a commit that referenced this issue Jul 27, 2020
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
@Robert-OP
Copy link

Similar problem here trying to getSession in App.getInitialProps as below and pass the pageProps.session in the AuthProvider as in the examples above (and next-auth's documentation). To be noted that I have the session when I use the client-side hook useSession(), but not when I try to getSession(ctx) in App.getInitialProps, neither in getServerSideProps function on another page (basically getting the same error as below).

So, it works well when I work with in local development, but when I deploy it to Azure in a Docker container I get the following error:

2021-02-18T12:08:04.140106140Z [next-auth][error][client_fetch_error]
2021-02-18T12:08:04.140215540Z https://next-auth.js.org/errors#client_fetch_error https://beta.femaleinvest.com/api/auth/session FetchError: invalid json response body at https://beta.femaleinvest.com/api/auth/session reason: Unexpected token  in JSON at position 0
2021-02-18T12:08:04.140224040Z at /usr/next_app/node_modules/node-fetch/lib/index.js:272:32
2021-02-18T12:08:04.140239040Z at runMicrotasks (<anonymous>)
2021-02-18T12:08:04.140252040Z at processTicksAndRejections (node:internal/process/task_queues:94:5)
App.getInitialProps = async ({ Component, ctx, router }: AppContext) => {
  const localeFilter = { name: 'locale', value: router.locale.replace('-', '_') };

  const footerData = await getStrapiSingle('footers', { filter: localeFilter });
  const headerData = await getStrapiSingle('headers', { filter: localeFilter });
  const session = await getSession(ctx);

  let pageProps = {};

  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  let editorMode = process.env.EDITOR_MODE === 'true';

  return {
    pageProps: { ...pageProps, headerData, footerData, editorMode },
  };
};

@neongreen
Copy link

neongreen commented Jan 6, 2022

For anyone looking for an example, here's mine:

import type { AppContext } from 'next/app'
import App from 'next/app'
import Layout from '../components/layout'
import { getSession, SessionProvider } from "next-auth/react"
import SSRProvider from 'react-bootstrap/SSRProvider'
// Not necessary if you aren't using SWR
import { SWRConfig } from 'swr'

function MyApp({ Component, pageProps, session }) {
  return (
    <SSRProvider>
      <SessionProvider session={session}>
        <SWRConfig
          value={{
            refreshInterval: 3000,
            fetcher: async (resource, init) => fetch(resource, init).then(async res => res.json())
          }}
        >
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </SWRConfig>
      </SessionProvider>
    </SSRProvider>
  )
}

MyApp.getInitialProps = async (appContext: AppContext) => {
  // perhaps getSession(appContext.ctx) would also work
  const session = await getSession({ req: appContext.ctx.req })
  const appProps = await App.getInitialProps(appContext)
  return { ...appProps, session }
}

export default MyApp

Now I can use const {data: session} = useSession() in my React components, including the layout component, and everything works without flashes of "logged out" on page visits/refreshes.

@balazsorban44
Copy link
Member

Keep in mind that getInitialProps will deoptimize the whole site. I would recommend something like this:

export default function App({Component, pageProps: {session, ...pageProps}}) {
  return (
    <SessionProvider session={session}>
      <Layout>
        <Component {...pageProps} />
      </Layout
    </SessionProvider>
  )
}

function Layout({ children }) {
  const session = useSession()
  if (session.status === "loading") {
    return "skeleton UI"
  } else if(session.status === "unauthenticated") {
    return "not logged in"
  }
  return "UI using session.data"
}

With a nicely implemented skeleton UI, you can minimize/eliminate the most undesired flashes/jumps. This will have a better perceived performance in my opinion.

@neongreen
Copy link

neongreen commented Jan 7, 2022

There's a button in the header that says "log in" or "log out". I'm not sure what you mean by skeleton UI but off the top of my head I don't know how the flash there could be hidden.

FWIW my site is a note-taking app and the only page that could even in theory be pre-rendered is the landing page, everything else is generated from user content. Or maybe I've also misunderstood what you mean by deoptimizing.

@balazsorban44
Copy link
Member

I see. Dashboards/user-focused sites are generally a good exception for this.

Skeleton UI

@neongreen
Copy link

neongreen commented Jan 13, 2022

I think I've finally come up with the right way to use getSession in getInitialProps. My previous example was wrong because it kept querying for /api/auth/session on every client-side page navigation.

Here is my current version:

function MyApp(props) {
  const { Component, pageProps } = props
  return (
    <SSRProvider>
      {/* If props.session isn't provided, MyApp is being rendered client-side. In this case
          we make sure to not provide any session at all so that the session cache maintained
          by the SessionProvider would be reused. */}
      <SessionProvider {...(props.session !== undefined) ? { session: props.session } : {}}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </SessionProvider>
    </SSRProvider>
  )
}

MyApp.getInitialProps = async (appContext: AppContext) => {
  let session: Session | null | undefined = undefined
  // getSession works both server-side and client-side but we want to avoid any calls to /api/auth/session
  // on page load, so we only call it server-side.
  if (typeof window === 'undefined')
    session = await getSession(appContext.ctx)
  const appProps = await App.getInitialProps(appContext)
  return { ...appProps, ...((session !== undefined) ? { session } : {}) }
}

@aidanCQ
Copy link

aidanCQ commented May 12, 2022

For those that are experiencing a flash of content when using a client side redirect this may help:

const AuthRedirect = ({ children }: { children?: React.ReactNode }) => {
  const { status } = useSession();
  const router = useRouter();
  React.useEffect(() => {
    if (status === "unauthenticated" && router.asPath !== "/auth/signIn") {
      signIn();
    }
  }, [status, router]);
  // Make sure that you show a loading state for BOTH loading and unauthenticated. 
  // This is because when status becomes `unathenticated` the component renders, 
  // returns children and then the useEffect redirect is fired afterwards,
  // hence the temporary flash of the child content.
  if (status === "loading" || status === "unauthenticated") {
    return <div>Loading...</div>
  }
  return <>{children}</>;
};

mnphpexpert added a commit to mnphpexpert/next-auth that referenced this issue Sep 2, 2024
Requested in nextauthjs#345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Ask how to do something or how something works
Projects
None yet
7 participants