In my Next.js project, I have a registration form as shown below:
"use client";
import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
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 { useRouter } from "next/navigation";
import { registerUser } from "@/lib/register-user";
export type FormData = z.infer<typeof userAuthSchema>;
export function RegisterForm() {
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 router = useRouter();
async function onSubmit(submittedData: FormData) {
await registerUser({submittedData, setError, setIsLoading, reset, router})
}
return (
<div>
{error && <p className="mb-5 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}
{...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">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={isLoading}
{...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" />
)}
Register
</button>
</div>
</form>
</div>
);
}
Notice that I am passing a router
argument to the registerUser()
function:
async function onSubmit(submittedData: FormData) {
await registerUser({submittedData, setError, setIsLoading, reset, router})
}
And here is the registerUser
function:
import "client-only";
import type { FormData } from "@/components/auth/register-form";
import { NextRouter } from "next/router";
const RESPONSE_MESSAGES = {
USER_DOCUMENT_CREATED: "New user document created",
USER_DOCUMENT_UPDATED: "Existing user document updated",
USER_ALREADY_REGISTERED: "User already registered",
EMAIL_VERIFICATION_LINK_SENT: "Email verification link sent",
EMAIL_VERIFICATION_LINK_ALREADY_SENT:
"Email verification link already sent. Please check your inbox and verify your email.",
REGISTRATION_FAILED: "Registration failed. Please try again.",
};
interface RegisterUserProps {
submittedData: FormData;
setError: React.Dispatch<React.SetStateAction<string | null>>;
reset: () => void;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
router: NextRouter;
}
export async function registerUser({
submittedData,
setError,
reset,
setIsLoading,
router,
}: RegisterUserProps) {
setIsLoading(true);
setError(null);
try {
// Attempt to register the user
const registerResponse = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(submittedData),
});
// Registration failed
if (!registerResponse.ok) {
throw new Error(RESPONSE_MESSAGES.REGISTRATION_FAILED);
}
const registerResponseData = await registerResponse.json();
// User already registered
if (
registerResponseData.message === RESPONSE_MESSAGES.USER_ALREADY_REGISTERED
) {
setError(RESPONSE_MESSAGES.USER_ALREADY_REGISTERED);
reset();
return;
}
// Email verification link was already sent
if (
registerResponseData.message ===
RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_ALREADY_SENT
) {
setError(RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_ALREADY_SENT);
reset();
return;
}
// New user. Send email verification link
if (
registerResponseData.message ===
RESPONSE_MESSAGES.USER_DOCUMENT_CREATED ||
registerResponseData.message === RESPONSE_MESSAGES.USER_DOCUMENT_UPDATED
) {
const { email, emailVerificationToken } = registerResponseData;
const emailResponse = await fetch(
"/api/send-registration-email-verification-link",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, emailVerificationToken }),
},
);
// Check if email was successfully sent
if (!emailResponse.ok) {
throw new Error(RESPONSE_MESSAGES.REGISTRATION_FAILED);
}
const emailResponseData = await emailResponse.json();
if (
emailResponseData.message ===
RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_SENT
) {
reset();
router.replace("/register/verify-email");
}
}
} catch (error) {
setError((error as Error).message);
} finally {
setIsLoading(false);
}
}
As you can see, I have type annotated router
with NextRouter
:
interface RegisterUserProps {
submittedData: FormData;
setError: React.Dispatch<React.SetStateAction<string | null>>;
reset: () => void;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
router: NextRouter;
}
However, when I do this, I am getting a red squiggly line under router
in the RegisterForm component.
The error says the following:
Type 'AppRouterInstance' is not assignable to type 'NextRouter'.
Type 'AppRouterInstance' is missing the following properties from type 'BaseRouter': route, pathname, query, asPath, and 2 more.ts(2322)
register-user.ts(20, 3): The expected type comes from property 'router' which is declared here on type 'RegisterUserProps'
(property) RegisterUserProps.router: NextRouter
Can someone explain me what's going on here? And how can I fix this TypeScript error?
Next.js has two useRouter
s
The old one can be imported from 'next/router' and the new one in v13+ is imported from 'next/navigation`
The old useRouter
type is NextRouter
and the new one is AppRouterInstance
You used the new in your component and typed the old in your function.
One tip I used to do for similar situations is to noy import the type but to let Typescript infer the type of the function by: UseRouter: typeof useRouter
and to get the the return type use the ReturnType
helper type router = ReturnType<typeof useRouter>
Because there is no real need for this type the Next.js team doesn't export it directly. But you can still import the internal type import { AppRouterInstance } from "next/dist/shared/lib/app-router-context"