'How can I set a flash variable in Next.js before a redirect?

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



Solution 1:[1]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1