1

Laravel in PHP made this easy with https://laravel.com/docs/9.x/session#flash-data, so I figured Next.js would have an easy way too.

I thought I'd be able to do something like:

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const session = await getSession(ctx);
  if (!session) {
   ctx.res.setHeader("yourFlashVariable", "yourFlashValue");
   console.log('headers', ctx.res.getHeaders()); // Why is it not even appearing here?

    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    };
  }

  const props = ...
  return { props };
};

and then in my other page:

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { headers, rawHeaders } = context.req;
  // look inside the headers for the variable
  // ...

But the header doesn't appear.

If you know how to achieve the goal of a flash variable (even if not using headers), I'm interested in whatever approach.

(Originally I asked How can I show a toast notification when redirecting due to lack of session using Next-Auth in Next.js? but now feel like I should have asked this more generic question.)


UPDATE

I appreciate the reasonable suggestion from https://stackoverflow.com/a/72210574/470749 so have tried it.

Unfortunately, index.tsx still does not get any value from getFlash.

// getFlash.ts

import { Session } from 'next-session/lib/types';

export default function getFlash(session: Session) {
  // If there's a flash message, transfer it to a context, then clear it.
  const { flash = null } = session;
  console.log({ flash });
  // eslint-disable-next-line no-param-reassign
  delete session.flash;
  return flash;
}
// getNextSession.ts

import nextSession from 'next-session';

export default nextSession();
// foo.tsx
import { getSession } from 'next-auth/react';
import { GetServerSideProps, InferGetServerSidePropsType, NextApiRequest, NextApiResponse } from 'next';
import getNextSession from '../helpers/getNextSession';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const session = await getSession(ctx);
  if (!session) {
    const req = ctx.req as NextApiRequest;
    const res = ctx.res as NextApiResponse;
    const nSession = await getNextSession(req, res);
    nSession.flash = 'You must be logged in to access this page.'; // THIS LINE CAUSES A WARNING
    console.log({ nSession });
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    };
  }
  // ...
  return { props };
};
// index.tsx
import { GetServerSideProps } from 'next';
import getFlash from '../helpers/getFlash';
import getNextSession from '../helpers/getNextSession';

export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await getNextSession(context.req, context.res);
  let toast = getFlash(session);
  console.log({ toast });
  if (!toast) {
    toast = 'no toast';
  }
  console.log({ toast });
  return {
    props: { toast }, // will be passed to the page component as props
  };
};

Also, the nSession.flash = line causes this warning:

warn - You should not access 'res' after getServerSideProps resolves. Read more: https://nextjs.org/docs/messages/gssp-no-mutating-res

Ryan
  • 22,332
  • 31
  • 176
  • 357

1 Answers1

4

Your first code is working fine for me (printing the headers in terminal). However, the combination will not work as intended because the headers you set in /foo (say) will be sent to browser, along with a status code of 307, and a location header of /. Now "the browser" will be redirecting to the location and it won't forward your headers. Similar threads: https://stackoverflow.com/a/30683594, https://stackoverflow.com/a/12883411.


To overcome this, you can do something like this. This works because the browser does send the cookies (in this case, set when you create a session).

// lib/session.ts

import type { IronSessionOptions } from 'iron-session'
import type { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'

export const sessionOptions: IronSessionOptions = {
  password: process.env.SECRET_COOKIE_PASSWORD as string,
  cookieName: 'sid',
  cookieOptions: { secure: process.env.NODE_ENV === 'production' },
}

declare module 'iron-session' {
  interface IronSessionData {
    flash?: string | undefined
  }
}

export const withSessionRoute = (handler: NextApiHandler) =>
  withIronSessionApiRoute(handler, sessionOptions)

export const withSessionSsr = <P extends Record<string, unknown> = Record<string, unknown>>(
  handler: (
    context: GetServerSidePropsContext
  ) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) => withIronSessionSsr(handler, sessionOptions)
// pages/protected.tsx

import type { NextPage } from 'next'
import { getSession } from 'next-auth/react'
import { withSessionSsr } from 'lib/session'

const ProtectedPage: NextPage = () => <h1>Protected Page</h1>

const getServerSideProps = withSessionSsr(async ({ req, res }) => {
  const session = await getSession({ req })
  if (!session) {
    req.session.flash = 'You must be logged in to access this page.'
    await req.session.save()
    return { redirect: { destination: '/', permanent: false } }
  }
  return { props: {} }
})

export default ProtectedPage
export { getServerSideProps }
// pages/index.tsx

import type { InferGetServerSidePropsType, NextPage } from 'next'
import { withSessionSsr } from 'lib/session'

const IndexPage: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = ({ flash }) => {
  // TODO: use `flash`
}

const getServerSideProps = withSessionSsr(async ({ req }) => {
  // if there's a flash message, transfer
  // it to a context, then clear it
  // (extract this to a separate function for ease)
  const { flash = null } = req.session
  delete req.session.flash
  await req.session.save()
  return { props: { flash } }
})

export default IndexPage
export { getServerSideProps }

This also works if you want to set flash data in an API route instead of pages:

import { withSessionRoute } from 'lib/session'

const handler = withSessionRoute(async (req, res) => {
  req.session.flash = 'Test'
  await req.session.save()
  res.redirect(307, '/')
})

export default handler

Complete example: https://github.com/brc-dd/next-flash/tree/with-iron-session

brc-dd
  • 10,788
  • 3
  • 47
  • 67
  • Thank you so much for this suggestion! I was so excited to see it and to try. I was able to install next-session and translate to TypeScript, but unfortunately the flash variable still doesn't work. You can see my attempt in my update to the question above. Thanks again for the idea, though. – Ryan May 12 '22 at 16:37
  • 1
    @Ryan Here is a complete demo app: https://github.com/brc-dd/next-flash, clone it, install deps, run dev. Everything is working fine for me: https://user-images.githubusercontent.com/40380293/168168790-fe30481c-0214-4507-8e9e-9e0c593b5339.mp4 – brc-dd May 12 '22 at 21:12
  • You are AMAZING! So generous! I cloned it just now, and it does indeed work. I'm still not clear why yours works and mine doesn't, but this obviously proves that it works, so I'll accept this answer. Once I figure out what I've been doing wrong, I'll share. Thanks again! You've made my day. – Ryan May 13 '22 at 13:43
  • 1
    It's working in my project now, too. I'm still actually not quite sure what the difference was. One change I made was from`getServerSideProps: GetServerSideProps = async (ctx)` to `export const getServerSideProps = async ({ req, res })`, but I don't think it should have mattered. In any case, it's working, and I really, really appreciated your help! – Ryan May 13 '22 at 18:47
  • Here is a follow-up question (if you happen to have a moment and are interested) about the fact that the session variable only seems to work from getServerSideProps of normal pages and doesn't seem to persist through a redirect caused by an API endpoint: https://stackoverflow.com/q/72234516/470749 – Ryan May 13 '22 at 19:23
  • @Ryan try this: https://github.com/brc-dd/next-flash/tree/with-iron-session. There were some issues with `next-session`. After switching to `next-iron-session` everything appears to be working fine. – brc-dd May 14 '22 at 10:35
  • You are SO helpful! Thank you so much for telling me about and demonstrating https://github.com/vvo/iron-session#session-wrappers It works in my project. I appreciate it. I tried to give you a 200 point bounty, but it says I will need to wait ~23 hours. I'll try again tomorrow. You could also post a short answer at https://stackoverflow.com/q/72234516/470749 and I'll accept it there too. Thanks again! – Ryan May 14 '22 at 15:47
  • @Ryan Better just close those two questions as duplicates as this now covers both of them. – brc-dd May 14 '22 at 19:23
  • 1
    Sounds fine with me. I closed them. Thanks! – Ryan May 14 '22 at 19:32