Query parameters disappearing Next.js

Viewed 33

I'm trying to create a link to unsubscribe from a newsletter using Next.js. The idea is, an unsubscribe link can be attached to the newsletter, with a format like

https://www.example.com/newsletter?reason=unsubscribe&id=jFD8vb6Jy855EPiKZ7NTPReNv8HYDzb4

with query parameters: reason (unsubscribing) and the id of the unsubscriber. So, when the page detected the reason being "unsubscribe," it calls a Lambda function that deletes that user, selected by their id, from our database. This works locally, but when testing it live and clicking the unsubscribe hyperlink from the newsletter email, the query parameters disappear. Instead of showing the entire link with query parameters, what is displayed is just

https://www.example.com/newsletter,

so the Lambda function can't remove the subscriber from the mailing list. I have a feeling this has something to do with SSR with Next.js, but can't figure out how to fix it. Thanks!

import Head from 'next/head'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import styled from 'styled-components'
import Button from '../components/Button'
import Footer from '../components/Footer'
import Input from '../components/Input'
import NewsletterState from '../components/NewsletterState'
import Page from '../components/Page'
import StandardNavbar from '../components/StandardNavbar'
import { media } from '../utils/media'

interface EmailPayload {
  email: string
}

export default function Newsletter () {
  const Router = useRouter()
  const [hasSuccessfullySentMail, setHasSuccessfullySentMail] = useState(false)
  const [hasErrored, setHasErrored] = useState(false)
  const { register, handleSubmit, formState } = useForm()
  const { isSubmitSuccessful, isSubmitting, isSubmitted, errors } = formState

  async function onSubmit (payload: EmailPayload) {
    try {
      const res = await fetch('/api/signUpNewsletter', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ ...payload }),
      })

      if (res.status !== 204) {
        setHasErrored(true)
      }
    } catch {
      setHasErrored(true)
      return
    }

    setHasSuccessfullySentMail(true)
  }

  const isSent = isSubmitSuccessful && isSubmitted
  const isDisabled = isSubmitting || isSent
  const isSubmitDisabled = Object.keys(errors).length > 0 || isDisabled

  if (Router.query.reason === 'unsubscribe') {
    async function unsubscribeNewsletter () {
      await fetch('/api/unsubscribeNewsletter', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: Router.query.id }),
      })
    }
    unsubscribeNewsletter()
    return (
      <>
        <Head>
          <title>{'Unsubscribe'}</title>
          <meta content="Unsubscribe from newsletter." />
        </Head>
        <StandardNavbar />

        <Page
          title='Unsubscribe Successful'
          description="You will no longer receive our newsletter."
        >
        </Page>
        <Footer />
      </>
    )
  } else {
    return (
      <>
        <Head>
          <title>{'Newsletter'}</title>
          <meta content="Sign up for our newsletter." />
        </Head>
        <StandardNavbar />

        <Page
          title="Newsletter"
          description='Enter your email to get weekly updates!'
        >
          <MainDiv>
            <Wrapper>
              {hasSuccessfullySentMail ? (
                <NewsletterState />
              ) : (
                <Form
                  // @ts-expect-error
                  onSubmit={handleSubmit(onSubmit)}
                >
                  {hasErrored && <ErrorMessage>Couldn&apos;t send email. Please try again.</ErrorMessage>}
                  <InputGroup>
                    <InputStack>
                      {errors.email && <ErrorMessage>Email is required</ErrorMessage>}
                      <Input placeholder='Email' id='email' disabled={isDisabled} {...register('email', { required: true })} />
                    </InputStack>
                  </InputGroup>
                  <SubmitButton as='button' type='submit' disabled={isSubmitDisabled}>
                    Sign Up!
                  </SubmitButton>
                </Form>
              )}
            </Wrapper>
          </MainDiv>
        </Page>
        <Footer />
      </>
    )
  }
}

const SubmitButton = styled(Button)`
  display: flex;
  margin-top: 2rem !important;
  margin: 0 auto;
`

const MainDiv = styled.div`
  padding: 5rem;
`

const Wrapper = styled.div`
  flex: 2;
  ${media('<=tablet')} {
    text-align: center;
  }
`

const Form = styled.form`
  & > * {
    margin-bottom: 2rem;
  }
`

const InputGroup = styled.div`
  display: flex;
  align-items: center;
  & > *:not(:last-child) {
    margin-right: 2rem;
  }
  & > * {
    flex: 1;
  }
  margin: 0 auto;
  max-width: 25vw;
  ${media('<=tablet')} {
    flex-direction: column;
    & > *:not(:last-child) {
      margin-right: 0rem;
      margin-bottom: 2rem;
      max-width: 90vw;
    }
  }
`

const InputStack = styled.div`
  display: flex;
  flex-direction: column;
  & > *:not(:first-child) {
    margin-top: 0.5rem;
  }
`

const ErrorMessage = styled.p`
  color: rgb(var(--errorColor));
  font-size: 1.5rem;
`
1 Answers

Could this be the caveat in the next docs?

Pages that are statically optimized by Automatic Static Optimization will be hydrated without their route parameters provided, i.e query will be an empty object ({}). After hydration, Next.js will trigger an update to your application to provide the route parameters in the query object.

Related