Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to type annotate searchParams in Next.js 13?

In my Next.js 13 project, I have a login form as shown below:

"use client";

import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { useSearchParams } from "next/navigation";
import Link from "next/link";

import { cn } from "@/lib/util";
import { userAuthSchema } from "@/lib/validations/auth";
import { buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Icons } from "@/components/icons";
import { loginUser } from "@/lib/login-user";

export type FormData = z.infer<typeof userAuthSchema>;

export function LoginForm() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormData>({ resolver: zodResolver(userAuthSchema) });

  const [isLoading, setIsLoading] = React.useState<boolean>(false);
  const [error, setError] = React.useState<string | null>(null);
  const [isGoogleLoading, setIsGoogleLoading] = React.useState<boolean>(false);

  const searchParams = useSearchParams();

  async function onSubmit(submittedData: FormData) {
    await loginUser({
      submittedData,
      setError,
      setIsLoading,
      reset,
      searchParams,
    });
  }

  return (
    <div className="grid gap-6">
      {error && (
        <p className="text-sm text-red-500 animate-in fade-in-0 slide-in-from-left-1">
          {error}
        </p>
      )}
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="grid gap-6">
          <div className="grid gap-1">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              placeholder="name@example.com"
              autoCapitalize="none"
              autoComplete="email"
              autoCorrect="off"
              disabled={isLoading || isGoogleLoading}
              {...register("email")}
            />
            {errors.email && (
              <p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
                {errors.email.message}
              </p>
            )}
          </div>
          <div className="grid gap-1">
            <div className="flex items-center justify-between">
              <Label htmlFor="password">Password</Label>
              <Link
                href="/request-password-reset"
                className="text-sm font-medium text-sky-700 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400 focus-visible:ring-offset-2 active:text-sky-400"
              >
                Forgot password?
              </Link>
            </div>
            <Input
              id="password"
              type="password"
              autoCapitalize="none"
              autoComplete="off"
              autoCorrect="off"
              disabled={isLoading || isGoogleLoading}
              {...register("password")}
            />
            {errors.password && (
              <p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
                {errors.password.message}
              </p>
            )}
          </div>
          <button className={cn(buttonVariants())} disabled={isLoading}>
            {isLoading && (
              <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
            )}
            Login
          </button>
        </div>
      </form>
      <div className="relative">
        <div className="absolute inset-0 flex items-center">
          <span className="w-full border-t border-neutral-200" />
        </div>
        <div className="relative flex justify-center text-xs uppercase">
          <span className="bg-neutral-100 px-2">Or</span>
        </div>
      </div>
      <button
        type="button"
        className={cn(buttonVariants({ variant: "outline" }), "w-full")}
        onClick={() => {
          setIsGoogleLoading(true);
          signIn("google", {
            callbackUrl: searchParams?.get("from") || "/blog",
          });
        }}
        disabled={isLoading || isGoogleLoading}
      >
        {isGoogleLoading ? (
          <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
        ) : (
          <Icons.google className="mr-2 h-6 w-6" />
        )}{" "}
        Login with Google
      </button>
    </div>
  );
}

As you can see, searchParams is one of the arguments passed to the loginUser() function inside the onSubmit handler. Here’s the relevant portion of the code:

  async function onSubmit(submittedData: FormData) {
    await loginUser({
      submittedData,
      setError,
      setIsLoading,
      reset,
      searchParams,
    });
  }

And here’s the loginUser() function:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

import type { FormData } from "@/components/auth/register-form";
import { signIn } from "next-auth/react";

const RESPONSE_MESSAGES = {
  USER_NOT_FOUND: "User not found",
  INVALID_PASSWORD: "Invalid password",
  LOGIN_SUCCESSFUL: "Logged in successfully",
  LOGIN_FAILURE: "Login failed. Please try again.",
  EMAIL_NOT_VERIFIED: "Please verify your email",
};

interface LoginUserProps {
  submittedData: FormData;
  setError: React.Dispatch<React.SetStateAction<string | null>>;
  setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
  reset: () => void;
  searchParams: any;
}

export async function loginUser({
  submittedData,
  setError,
  setIsLoading,
  reset,
  searchParams,
}: LoginUserProps) {
  setIsLoading(true);
  setError(null);

  try {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(submittedData),
    });

    const responseData = await response.json();

    if (!response.ok) {
      throw new Error(RESPONSE_MESSAGES.LOGIN_FAILURE);
    }

    if (responseData.message === RESPONSE_MESSAGES.USER_NOT_FOUND) {
      setError(RESPONSE_MESSAGES.USER_NOT_FOUND);
      return;
    }

    if (responseData.message === RESPONSE_MESSAGES.EMAIL_NOT_VERIFIED) {
      setError(RESPONSE_MESSAGES.EMAIL_NOT_VERIFIED);
      return;
    }

    if (responseData.message === RESPONSE_MESSAGES.INVALID_PASSWORD) {
      setError(RESPONSE_MESSAGES.INVALID_PASSWORD);
      return;
    }

    // Use signIn just to log in, and use router.push for redirection.
    if (responseData.message === RESPONSE_MESSAGES.LOGIN_SUCCESSFUL) {
      reset(); // Reset the form state

      // Sign-in and let it handle the redirection
      signIn("credentials", {
        ...submittedData,
        callbackUrl: searchParams?.get("from") || "/blog",
      });
    }
  } catch (error) {
    setError((error as Error).message);
  } finally {
    setIsLoading(false);
  }
}

As you can see, I have currently typed searchParams as any. Of course, I don’t want to do that. Here is the relevant portion of the code:

interface LoginUserProps {
  submittedData: FormData;
  setError: React.Dispatch<React.SetStateAction<string | null>>;
  setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
  reset: () => void;
  searchParams: any;
}

Can someone tell me what would be the type of searchParams? Thanks…

>Solution :

The return type of useSearchParmas is ReadonlyURLSearchParams in the App dir and ReadonlyURLSearchParams | null in the pages directory.

The ReadonlyURLSearchParams is imported from next/navigation

import { ReadonlyURLSearchParams } from "next/navigation"
// ...
const searchParams = useSearchParams();
// ?^ searchParams: ReadonlyURLSearchParams 
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading